胤的博客
CC BY-NC-SA 4.0

©️ 2023 胤 版权所有

banner

所念皆星河

avatar

React:封装 Context

2023.08.26

最近在公司写项目时,发现有一些可复用的代码没有封装,好多组件中都使用了后端传回的站点数据,这个数据是相同的,但是每个组件在使用这个数据之前都向后端接口发送了请求,明明只要一个请求就能搞定,现在的结果是产生了许多不必要的请求。通过查阅 React 官方文档,我发现使用 context 可以只请求一次站点数据,并提供给所有组件使用。

React 官方给出了 context 的基本用法

import { createContext, useContext } from "react";

const themeContext = createContext<string | null>(null);

export default App = () => {
  return (
    <themeContext.Provider value="light">
      <Body />
    </themeContext.Provider>
  );
};

const Body = () => {
  const theme = useContext(themeContext);
  return <div>{theme}</div>;
};

在这个例子中,context 的值如果是通过 App 组件中 useState 创建的,那么该值的变动会导致所有子组件的重新渲染,因为整个 App 组件重新渲染了:

import { createContext, useContext, useState } from "react";

const themeContext = createContext<string | null>(null);

export default function App() {
  const [theme, setTheme] = useState("light");
  const handleClick = () => {
    theme === "light" ? setTheme("dark") : setTheme("light")
  }
  return (
    <themeContext.Provider value={theme}>
      <button onClick={handleClick}>Change Theme</button>
      <Body />
      <!-- NoRerender still re-render -->
      <NoRerender />
    </themeContext.Provider>
  );
};

const Body = () => {
  const theme = useContext(themeContext);
  return <div>{theme}</div>;
};

const NoRerender = () => {
  return <div>No re-render</div>;
};

更好的办法是单独封装一个 context,在 context 里面处理 useState 的值,并通过 children 添加其他组件。由于 children 来自 App 组件,所以 ThemeProvider 的 props 并没有发生改变,只有使用 context 的组件会重新渲染:

// ThemeContext.tsx

import { ReactNode, createContext, useContext, useState } from "react";

type themeContextProps = {
  theme: string;
  setTheme: (v: string) => void;
};

const themeContext = createContext<themeContextProps | null>(null);

const useTheme = () => {
  const context = useContext(themeContext);

  if (!context) {
    throw new Error("You can only use 'useTheme' in ThemeProvider");
  }

  return context;
};

const ThemeProvider = ({ children }: { children: ReactNode }) => {
  const [theme, setTheme] = useState("light");

  return (
    <themeContext.Provider value={{ theme, setTheme }}>
      {children}
    </themeContext.Provider>
  );
};

export { useTheme, ThemeProvider };
// App.tsx

import { useTheme, ThemeProvider } from "./ThemeContext";

export default function App() {
  return (
    <ThemeProvider>
      <Body />
      <!-- NoRerender not re-render -->
      <NoRerender />
    </ThemeProvider>
  );
}

const Body = () => {
  const { theme, setTheme } = useTheme();

  const handleClick = () => {
    theme === "light" ? setTheme("dark") : setTheme("light");
  };
  return (
    <div>
      <div>{theme}</div>
      <button onClick={handleClick}>Change Theme</button>
    </div>
  );
};

const NoRerender = () => {
  return <div>No re-render</div>;
};

通过这种改造,能够更好地将 context 的逻辑抽离出来,使它不影响未使用 context 的组件。使用 context 可以更方便在多组件间通讯,但每个 context 的使用范围应当明确界定,如果将 user 和 admin 的数据存在同一个 context 中,对 user 数据的修改会导致所有使用 admin 数据的组件重新渲染。

题外话:之后我也会在博客中更新一些技术类的文章,工作之后发现有些刚学的技术如果不记录下来,没多久就忘了,记录的时候能把相应逻辑的代码重新打一遍,这可以加深理解。

评论