如何正确使用 React Context

ℹ️ 本文发布于 2022-10-14 请注意文中内容的时效性。

正常情况下,可以直接使用 props 来传递数据到子组件。props 是 React 中最直接的数据传递方式,适用于大多数场景。

当后代组件层级很深,且只有一个后代组件使用到该 props,你应该使用组件组装(component composition)。这种方式可以减少中间组件的冗余代码,同时保持组件的可维护性。

当后代组件层级很深,且有多个后代(同级或不同级)组件需要访问相同的数据时,你应该使用 ContextContext 提供了一种在组件树中传递数据的方式,而无需手动逐层传递 props

组件组装(Component Composition)

组件组装是一种通过组合组件来传递数据的方式,而不是通过层层传递 props。这种方式可以减少中间组件的冗余代码,同时保持组件的可维护性和可复用性。

场景说明

假设有一个 Page 组件,它需要将 useravatarSize 传递给一个嵌套很深的 Avatar 组件。如果直接传递 props,代码会变得冗余且难以维护:

<Page user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ... 渲染出 ...
<Link href={user.permalink}>
  <Avatar user={user} size={avatarSize} />
</Link>

组件组装解决方案

通过将 Avatar 组件直接传递给中间组件,避免了层层传递 props

function Page({ user, avatarSize }) {
  const userLink = (
    <Link href={user.permalink}>
      <Avatar user={user} size={avatarSize} />
    </Link>
  );
  return <PageLayout userLink={userLink} />;
}

// 中间组件无需关心 `user` 和 `avatarSize`
function PageLayout({ userLink }) {
  return (
    <div>
      <NavigationBar />
      {userLink}
    </div>
  );
}

function NavigationBar() {
  return <div>Navigation</div>;
}

function Link({ href, children }) {
  return <a href={href}>{children}</a>;
}

function Avatar({ user, size }) {
  return <img src={user.avatarUrl} alt={user.name} style={{ width: size, height: size }} />;
}

这种方式减少了 props 的传递层级,使代码更加简洁,同时保持了组件的解耦。

如何使用 Context?

创建 Context

const SomeContext = React.createContext('default value');

React.createContext 创建一个 Context 对象。defaultValue 参数仅在组件树中没有匹配的 Provider 时生效,可用于测试或默认行为。

父组件挂载

当需要在使用时改变 context 值时,就使用 Provider 挂载:

<SomeContext.Provider value={value}>
  <MyComponent />
</SomeContext.Provider>

提示:当不需要改变 context 的值时,你可以不使用 Provider,直接使用默认值。https://github.com/facebook/react/issues/17912

如果定制化代码较多,建议将 Provider 抽到单独文件:

export const OrderProvider = ({ children }) => {
  const [total, setTotal] = useState(0);

  const onAdd = (num) => {
    setTotal(total + num);
  };

  return <OrderContext.Provider value={{ total, onAdd }}>{children}</OrderContext.Provider>;
};

使用此 Provider 文件来挂载:

import { OrderProvider } from './provider';

export default function App() {
  return <OrderProvider>your app here</OrderProvider>;
}

后代组件使用

Function Component 方式

function MyComponent() {
  const value = React.useContext(SomeContext);
  return <span>{value}</span>;
}

Class Component 方式

使用 contextType 来读取,然后通过 this.context 来使用:

class MyComponent extends React.Component {
  static contextType = SomeContext;
  render() {
    return <span>{this.context}</span>;
  }
}

注意事项

  1. 性能问题:Context 的变化会导致所有订阅的组件重新渲染。如果需要优化性能,可以结合 React.memouseMemo 来避免不必要的渲染。
  2. 默认值的使用:默认值仅在没有 Provider 时生效,因此在生产环境中,建议始终使用 Provider 包裹组件。
  3. 组件复用性:使用 Context 时,组件的复用性可能会降低,因为它们依赖于外部的上下文。因此,仅在必要时使用 Context。

结论

在选择数据传递方式时,优先顺序如下:

  1. 使用 props,适用于简单的父子组件数据传递。
  2. 使用组件组装(component composition),适用于深层嵌套但只有一个组件需要数据的场景。
  3. 使用 Context,适用于多个后代组件需要共享数据的场景。

参考