一、useContext 是什么?

useContext 是 React 的一个 Hook,它允许你在组件树中跨多层级访问 context 的值,而无需通过每层手动传递 props。

二、useContext 使用场景

例如,可以在应用中使用 useContext 来访问用户认证信息、主题设置、语言偏好等全局状态,而不必在每个组件中手动传递这些信息。这样可以简化组件的逻辑,使代码更加清晰和易于维护。
举个例子:父组件A里面定义的状态state1,它的子组件B需要用到父组件的状态state1,通常我们都会通过父组件向子组件传入 props 属性,借助 props 进行父子通信。如果此时,子组件B中又有一个子组件C,子组件C中又有子组件D……等,这些组件都需要父组件A中的状态state1,那么我们该怎么办呢?可能还是会有些人想着用 props 一层层的向下传递,这种方式在这种场景下存在如下弊端

  1. 嵌套过深:随着数据向下传递,组件树会变得非常深,导致代码结构复杂,难以维护和理解。

  2. 组件耦合度高:如果需要传递的数据在多个层级的组件中都要用到,那么就需要在每个中间组件中传递这些数据,导致组件之间的耦合度增加。

  3. 组件复用性差:如果某个中间组件不需要使用传递下来的数据,但是为了传递给子组件而接收这些数据,会导致组件的复用性变差。

维护困难:当需要修改传递的数据时,需要逐层去修改传递的代码,容易出错且不易维护。

因此,使用 props 一层层地向下传递数据会导致代码结构复杂、耦合度高、维护困难等问题。而 useContext 提供了一种更方便、更高效的方式来在组件之间共享数据,能够更好地解决这些问题。

注:Redux、Mobx 等三方状态管理库也适合解决此类问题,但是本文主要探讨使用 React 自带的 hook 来做状态管理。

三、使用步骤

1.使用 createContext 创建一个 Context

首先需要创建一个 Context 对象。这通常在组件树的顶层完成。

javascript(示例):

import React from 'react';

const MyContext = React.createContext(defaultValue);

这里的 defaultValue 是当组件上层没有匹配的 Provider 时,context 的默认值。

(怎么理解这句话?简单地说,如果没有相应的 Provider 去包裹当前子组件, 那么 useContext 将会返回你在创建 Context 时传入的 defaultValue 。Context 的 Provider 为子组件树提供一个值,当你把组件放入 Provider 内部时,你可以通过 useContext 获得这个值,而不用传递 props。如果组件不在 Provider 内部,useContext 将会返回创建 Context 时提供的 defaultValue。继续往下阅读,再回过头来看这句话你就会明白了。)

2.使用 Provider 提供值

在组件树中用 Provider 包裹住它的子组件,以提供 context 的当前值给这些子组件。

javascript(示例):

<MyContext.Provider value={/* 某个值 */}>
  {/* 子组件 */}
</MyContext.Provider>

通过将 value 属性设置为你希望在组件树中共享的数据,你可以在子组件中访问这个值。

扩展Provider 是由步骤1中 createContext 后返回 context 对象中的一个属性,可以看作一个 React 组件,它用于创建一个 Context,并将其提供给后代组件使用。Provider 组件接收一个 value 属性,用于传递给后代组件的数据。当后代组件使用 useContext Hook 时,它们会从最近的 Provider 中获取到对应的数据。是的,你没听错,从最近的 Provider 中获取,意味着 Provider 可以嵌套。文章后面会有相关示例代码。


3.使用 useContext 访问 Context

在你需要访问 context 的子组件中,使用 useContext Hook 来读取 context 的当前值。

import React, { useContext } from 'react';

function MyComponent() {
  const contextValue = useContext(MyContext);
  return <div>{contextValue}</div>;
}

你必须将 createContext() 时返回的对象作为参数传递给 useContext。(上述例子中,创建的 context 对象叫 ‘MyContext’ ,所以这里就传 ‘MyContext’)

完整示例

import React, { useContext, createContext } from 'react';

// 创建一个 Context 对象
const MyContext = createContext('defaultValue');

function App() {
  // 使用 Provider 包裹子组件,并提供 "newValue" 作为 context 值
  return (
    <MyContext.Provider value="newValue">
      <MyComponent />
    </MyContext.Provider>
  );
}

function MyComponent() {
  // 在子组件内使用 useContext 获取 context 的值
  const contextValue = useContext(MyContext);

  return (
    <div>
      Context Value: {contextValue}
    </div>
  );
}

export default App;

在这个示例中,如果你将 <MyComponent /> 放在 <MyContext.Provider value="newValue"> 之外,则 MyComponent 组件中使用的 contextValue 将是默认值 'defaultValue'。相反,如果放在 Provider 里面,则 contextValue 为提供的值 "newValue"。如果放在了 Provider 里面,但是 Provider 没有提供 value,则 contextValue 仍是默认值 'defaultValue'

四、Provider 的 value 类型

Provider 的 value 可以是任何 JavaScript 数据类型,包括但不限于:

基本数据类型:例如字符串、数字、布尔值等。
对象:可以是普通对象、数组、函数等。
复杂数据结构:例如嵌套对象、数组、Map、Set 等。
函数:可以是普通函数、箭头函数等。
需要注意的是,当 Provider 的 value 发生变化时,所有使用该 Context 的后代组件都会重新渲染。因此,在使用时需要注意避免在每次渲染时都创建新的对象或函数,以免导致不必要的重新渲染。

五、如何在子组件中修改 context 的数据?

只需要在顶层组件中,定义一个修改状态的方法,通过 Provider 的 value 传给子组件,在子组件中调用该方法即可。

javascript(示例):

import React, { useState, createContext, useContext } from 'react';

// 创建 Context 并附上默认值
const MyContext = createContext({
  user: 'Guest',
  isAuthenticated: false,
  setUser: () => {}
});

function App() {
  const [user, setUser] = useState('');
  const [isAuthenticated, setIsAuthenticated] = useState(false);
  return (
    <MyContext.Provider value={{ user, isAuthenticated, setUser, setIsAuthenticated }}>
      {/* 子组件 */}
      <MyComponent />
    </MyContext.Provider>
  );
}

function MyComponent() {
  // 使用 useContext 获取 context 的值
  const { user, isAuthenticated, setUser, setIsAuthenticated } = useContext(MyContext);

  // 修改 context 中的值
  const onclick = () => {
	setUser("李逍遥")
  }

  return (
    <div>
      <button onClick={onclick}>点击修改用户名称</button>
      User: {user} <br />
      isAuthenticated: {isAuthenticated ? 'Yes' : 'No'}
      {/* 其他渲染逻辑 */}
    </div>
  );
}

注意:当使用 React 的 useContext 时,如果 context 的值发生变化所有使用该 context 的子组件都会重新渲染。这在某些情况下可能导致性能问题,特别是当有很多组件依赖于同一个 context 并且这些组件的渲染开销很大时。因此,引发了下面的思考。

六、使用 useContext 的考量

上面多次提到,Provider 中 value 值发生变化,导致内部所有子组件重新渲染,那么使用 useContext 进行状态管理是否还合理呢?
答案肯定是合理的。
使用 useContext 做组件树中跨多层级访问数据是合理的,尤其是在以下情况:

  1. 共享的数据不经常变化:如果共享的数据不是频繁变动的,那么使用 useContext 是合适的。
  2. 共享的数据量不大:如果你只是共享一些基本的数据,比如用户的登录状态,主题设置等,那么使用 useContext 是适当的。

总之要合理的去使用 useContext ,而不是毫无顾忌地过度使用。

七、如何避免 useContext 带来不必要的重新渲染?

要减少不必要的渲染,你可以采取以下措施:

  1. 拆分 Context:如果你的 context 包含了多个独立变化的值,考虑将它们拆分成多个独立的 context。这样,当某个特定部分的数据发生变化时,只有依赖于那部分数据的组件会重新渲染。

    const UserContext = createContext(userDefaultValue);
    const SettingsContext = createContext(settingsDefaultValue);
    
    <div>
      <UserContext.Provider value={userValue}>
        // ...只需要 userValue 数据的子组件
      </UserContext.Provider>
      <SettingsContext.Provider value={settingsValue}>
        // ...只需要 settingsValue 数据的子组件
      </SettingsContext.Provider>
    </div>
    
  2. 使用 React.memoshouldComponentUpdate:对于类组件,可以使用 shouldComponentUpdate 生命周期方法。对于函数组件,可以使用 React.memo 来避免不必要的渲染。

    const MyComponent = React.memo(function MyComponent(props) {
      // 渲染组件
    });
    

    React.memo 只会在组件的 props 发生变化时才会重新渲染组件。

  3. 精细管理 state:确保不是每次都更新整个 context 对象。使用 useReducer 或者将 state 分散到多个 useState 调用中,这样就只有相关的数据变化时才会触发更新。

通过这些方法,你可以有效地管理依赖于 context 的组件的渲染行为,同时还保持了 context 作为状态共享的优势。不过,每种方法都有其适用场景,需要根据具体情况选择使用。

八、Provider 的嵌套使用

多个 Context Provider 可以嵌套使用。在 React 中,这是一种常见的模式,用于将不同的数据和行为分散到组件树的不同层级。这样做可以创建多个独立的上下文环境,每个环境负责管理其自己的数据和逻辑,也是说对 context 进行模块化管理

javascript(示例):

<MyContext.Provider value={theme}>
	// ...子组件
	<UserContext.Provider value={userInfo}>
		// ...子组件
	</UserContext.Provider>
</MyContext.Provider>

这种模式的优点在于,它使得状态管理更加模块化和清晰。组件可以自由地选择订阅其中的一个或多个上下文,而不是从一个庞大、混杂的上下文对象中获取所需的所有信息。

注意事项

  1. 性能考量:虽然可以嵌套多个 Provider,但这也意味着组件树将变得更加复杂。如果每个 Provider 都管理着大量的状态,那么任何状态变化都可能导致大范围的组件重新渲染。因此,合理规划和组织你的上下文结构是非常重要的。

  2. 上下文分离:尽可能保持不同上下文的独立性。如果两个上下文之间存在强依赖关系,这可能是重新考虑你的状态管理策略的一个信号。

  3. 访问上下文:在子组件中,你可以通过 useContext Hook 来访问这些上下文。每个上下文提供的是其独立的数据和功能,这样你就可以在组件中灵活地选择所需的上下文。

总结

useContext 只在函数组件或自定义 Hook 中有效。
当 context 值变化时,所有使用 useContext Hook 的组件都将重新渲染。

使用 useContext 可以帮助你避免复杂的组件结构并简化数据传递。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐