精读React hooks(三):useContext从基础应用到性能优化

🧑‍💻
推荐全栈学习资源:
  • Next.js 中文文档:样式和官网一样的中文文档,创造沉浸式Next.js中文学习体验。
  • 《Chrome插件全栈开发》:真实出海项目的实战教学课,讲解Chrome插件和Next.js端的全栈开发,帮助你半个月内成为全栈出海工程师。
  • 上一篇文章(全面掌握useReducer)我们用useReduceruseContext实现了一个切换主题的功能,useReducer我们已经掌握了,那么本文就来学习useContext

    我们知道,每一个hook的诞生都有它独特的使命,比如useState是解决组件内状态更新的问题,useReducer是解决组件复杂状态更新的问题。

    useContext要解决的是:让开发者在某些场景下,从多层嵌套传值的组件树中解脱出来useContext实现的是:让开发者通过context实现跨层级共享状态

    基础用法

    1. 创建 Context

    首先,我们需要使用React.createContext创建一个context对象:

    const MyContext = React.createContext(defaultValue);

    这里的defaultValue是当组件不在任何 Context Provider 内部时的默认值,defaultValue可以用 null,但 React 官方建议提供一个有意义的默认值,这样可以让调用usecontext组件更安全。

    1. 使用 Context Provider

    为了在组件树中使用这个context,我们需要使用<MyContext.Provider>组件,它接受一个valueprop,这就是你想在它的子组件中共享的值。

    <MyContext.Provider value={someValue}>
      {/* 子组件 */}
    </MyContext.Provider>
    1. 在组件中访问 Context

    在函数组件中,可以使用useContexthook 来访问这个 context 的值。

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

    这里的contextValue就是第二步传入的someValue,而且contextValue获取到的永远是最新的值。

    一个示例

    我们来看一个更直观的示例:

    import React, { useContext } from 'react';
     
    // 1. 创建 Context
    const ThemeContext = React.createContext('light');
     
    function App() {
      return (
        // 2. 使用 Context Provider
        <ThemeContext.Provider value="dark">
          <Toolbar />
        </ThemeContext.Provider>
      );
    }
     
    function Toolbar() {
      return (
        <div>
          <ThemeButton />
        </div>
      );
    }
     
    function ThemeButton() {
      // 3. 在组件中访问 Context
      const theme = useContext(ThemeContext);
      return <button>{theme} theme</button>;
    }
     
    export default App;

    这个示例中,App 中引用了ThemeContext并传了值,ThemeButton 是 App 的孙组件,这二者之间没有通过 Toolbar 进行嵌套传值,但是 ThemeButton 依然通过useContext拿到了 App 里的值。

    从这个示例中我们可以总结出,React.createContextuseContext共同组成了一个管道,通过这个管道,开发者可以进行跨组件共享状态。

    进阶技巧

    动态Context值

    有的时候 Context 传的值需要动态变化,我们可以基于useState去更新状态,更新后的值会实时反应到调用 Context 的组件上。

    function ThemeProvider({ children }) {
      const [theme, setTheme] = useState("light");
     
      return (
        <ThemeContext.Provider value={theme}>
          <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
            Toggle Theme
          </button>
          {children}
        </ThemeContext.Provider>
      );
    }

    如果要更新对象也是可以的:

    import React, { useContext } from 'react';
     
    // 1. 创建 Context
    const CurrentUserContext = React.createContext('light');
     
    function App() {
    	const [currentUser, setCurrentUser] = useState(null
      return (
        // 2. 使用 Context Provider
        <CurrentUserContext.Provider 
    			value={{
            currentUser,
            setCurrentUser
          }}
    		>
          <Toolbar />
        </CurrentUserContext.Provider>
      );
    }
     
    function Toolbar() {
      return (
        <div>
          <LoginButton />
        </div>
      );
    }
     
    function LoginButton() {
      const {
        currentUser,
        setCurrentUser
      } = useContext(CurrentUserContext);
     
      if (currentUser !== null) {
        return <p>You logged in as {currentUser.name}.</p>;
      }
     
      return (
        <Button onClick={() => {
          setCurrentUser({ name: 'Advika' })
        }}>Log in as Weijunext</Button>
      );
    }
     
    export default App;

    和useReducer共用

    在大型应用中,通常会将useContextuseReducer结合起来使用,以便从组件中提取与某些状态相关的逻辑。上一篇文章即是用这种思路实现了主题切换,源码可查看Github:useReducer-useContext实现主题切换,本文不再重复。

    覆盖Provider value

    当我们调用多个相同 Context,会实现value的覆盖

    <ThemeContext.Provider value="dark">
      ...
      <ThemeContext.Provider value="light">
        <Footer />
      </ThemeContext.Provider>
      ...
    </ThemeContext.Provider>

    高级应用——性能优化

    当我们在使用useContext时,一个经常被提到的问题是性能优化。如果不正确地使用,Context 可能导致不必要的组件渲染,从而影响应用性能。

    为什么会出现性能问题?

    Providervalue属性值发生变化时,所有使用了useContext的组件都将重新渲染。如果value经常变化,或者消费者组件很多,那么这会引起大量的不必要的渲染。

    怎样解决?

    • 粒度化 Context

      如果你的 context 包含许多不同的状态值,尝试将它们分解成更小的 context。例如,而不是只有一个大的 AppContext,你可以有 UserContext、ThemeContext 等。这样,当某一部分的数据发生变化时,只有依赖于那部分数据的组件会重新渲染。

      import { createContext, useContext, useState } from 'react';
       
      const ThemeContext = createContext(null);
      const CurrentUserContext = createContext(null);
       
      export default function MyApp() {
        const [theme, setTheme] = useState('light');
        const [currentUser, setCurrentUser] = useState(null);
        return (
          <ThemeContext.Provider value={theme}>
            <CurrentUserContext.Provider
              value={{
                currentUser,
                setCurrentUser
              }}
            >
      	      <Toolbar />
      				<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
      	        Toggle Theme
      	      </button>
            </CurrentUserContext.Provider>
          </ThemeContext.Provider>
        )
      }
    • 使用多个 Context 提供者

      这与上一点相似。你可以为应用中的不同部分使用不同的 context 提供者,确保仅当真正需要的数据更改时才重新渲染组件。

    • 使用useMemouseCallback优化value

      为了避免value变化造成子孙组件频繁的重新渲染,可以使用useMemouseCallback对参数和方法进行缓存,减少value的无意义更新。

      import { useCallback, useMemo } from 'react';
       
      function MyApp() {
        const [currentUser, setCurrentUser] = useState(null);
       
        const login = useCallback((response) => {
          storeCredentials(response.credentials);
          setCurrentUser(response.user);
        }, []);
       
        const contextValue = useMemo(() => ({
          currentUser,
          login
        }), [currentUser, login]);
       
        return (
          <AuthContext.Provider value={contextValue}>
            <Page />
          </AuthContext.Provider>
        );
      }

    注:如果你的应用状态经常发生变化,并触发大量组件的更新,那么这种情况不适合用useContext,请立即考虑其它状态更新方案。

    结语

    希望本文所讲的useContext应用和技巧能帮助你掌握useContexthook。

    专栏资源

    专栏博客地址:精读React Hooks

    专栏演示站:React Hooks Demos

    专栏源码仓库:👉Github - Source Code

    交个朋友:👉加入「独立全栈交流群」

    专栏文章列表:

    精读React hooks(一):useState 的几个基础用法和进阶技巧

    精读React hooks(二):React状态管理的强大工具——useReducer

    精读React hooks(三):useContext从基础应用到性能优化

    精读React hooks(四):useRef的多维用途

    精读React hooks(五):useEffect使用细节知多少?

    精读React hooks(六):useLayoutEffect解决了什么问题?

    精读React hooks(七):用useMemo来减少性能开销

    精读React hooks(八):我们为什么需要useCallback

    精读React hooks(九):使用useTransition进行非阻塞渲染

    精读React hooks(十):使用useDeferredValue延迟状态更新

    精读React hooks(十一):useInsertionEffect——CSS-in-JS样式注入新方式

    精读React hooks(十二):使用useImperativeHandle能获得什么能力

    精读React hooks(十三):使用useSyncExternalStore获取实时数据

    精读React hooks(十四):总有一天你会需要useId为你生成唯一id

    精读React hooks(十五):把useDebugValue加入你的React调试工具库

    精读React hooks(十六):一个为代码优雅而生的hook——use