React中的use-context-selector库,useContextSelector 如何使用?

2,504 阅读5分钟

useContextSelector 是一个 React 钩子(hook)来自于 use-context-selector 库,这个库提供了一种方法来优化性能,通过避免在 React 的 Context API 的消费者组件中在上下文值未发生变化的情况下重新渲染。

使用 useContextSelector 的步骤如下:

  1. 安装 use-context-selector 库:
    如果你还没有安装这个库,你可以使用 npm 或 yarn 来安装它:

    npm install use-context-selector
    # 或者使用 yarn
    yarn add use-context-selector
    
  2. 创建上下文 (Context) :
    首先,你需要创建一个上下文,通常这是全局完成的,以便在整个组件树中可用。

    import { createContext } from 'use-context-selector';
    
    const MyContext = createContext(null);
    
  3. 提供上下文值 (Context Provider) :
    在组件树中的某个高层级位置,使用 MyContext.Provider 来提供上下文数据。

    <MyContext.Provider value={/* 上下文值,比如 state 对象 */}>
      {/* 应用的其他部分 */}
    </MyContext.Provider>
    
  4. 消费上下文中的特定值:
    在需要订阅上下文中特定部分数据的组件内,使用 useContextSelector

    import { useContextSelector } from 'use-context-selector';
    
    const myValue = useContextSelector(MyContext, context => context.partOfContext);
    
    • MyContext 是你创建的上下文对象。
    • 第二个参数是一个选择器函数,这个函数接收上下文值作为参数,并返回这个上下文中你感兴趣的那部分数据。
  5. 更新上下文中的值:
    如果上下文中的某些数据需要更新,可以提供一个函数作为上下文的一部分,然后在子组件中通过 useContextSelector 的钩子来调用那个函数更新上下文中的值。 例如,假设你的上下文包含了用户信息和一个用来更新这些信息的函数:

// UserContext.js
import { createContext } from 'use-context-selector';

const UserContext = createContext(null);

export default UserContext;

然后,你在应用的顶层组件设置 Provider,并传递一个用户对象和一个更新用户信息的函数:

// App.js
import React, { useState } from 'react';
import UserContext from './UserContext';

const App = () => {
  const [user, setUser] = useState({ name: 'John Doe', age: 30 });

  const updateUser = (newInfo) => {
    setUser(prevUser => ({ ...prevUser, ...newInfo }));
  };

  return (
    <UserContext.Provider value={{ user, updateUser }}>
      {/* 应用的其他部分 */}
    </UserContext.Provider>
  );
};

export default App;

在组件内部使用 useContextSelector 来选择并订阅上下文中的特定值,例如用户的名字:

// UserNameDisplay.js
import React from 'react';
import { useContextSelector } from 'use-context-selector';
import UserContext from './UserContext';

const UserNameDisplay = () => {
  // 只订阅用户名这一部分上下文
  const userName = useContextSelector(UserContext, (state) => state.user.name);

  return <p>User name is: {userName}</p>;
};

export default UserNameDisplay;

如果你还想要在组件中更新上下文值,可以再选择更新函数:

// UserAgeUpdater.js
import React from 'react';
import { useContextSelector } from 'use-context-selector';
import UserContext from './UserContext';

const UserAgeUpdater = () => {
  // 选择更新函数
  const updateUser = useContextSelector(UserContext, (state) => state.updateUser);

  // 调用更新函数来更新用户年龄
  const handleAgeUpdate = () => {
    updateUser({ age: 31 }); // 假设我们将用户年龄更新为 31
  };

  return <button onClick={handleAgeUpdate}>Update Age</button>;
};

export default UserAgeUpdater;

在 UserAgeUpdater 组件中,我们通过 useContextSelector 获得了 updateUser 函数,这允许用户在点击按钮后调用该函数来更新年龄。请注意,useContextSelector 不仅可以选择上下文中的数据,也可以选择上下文中提供的函数。

本例中,我们仅依赖用户年龄的更新,因此 UserNameDisplay 组件在这个更新发生时不会重新渲染,因为它只订阅了用户名的部分。这就是 useContextSelector 的核心优势所在 —— 它确保仅在组件实际需要的上下文部分变化时才触发重渲染。

这种细粒度的订阅模式,尤其是在上下文对象包含多个独立字段时,表现出显著的性能优势,因为它减少了组件不必要的重渲染次数。例如,当 user.name 不变,而 user.age 发生变化的时候,只有依赖 user.age 的组件会重新渲染,依赖 user.name 的组件不会受到影响。

总的来说,useContextSelector 是一个非常有用的工具,可以帮助你优化使用 React Context 的应用性能,特别是在复杂的应用中,上下文数据频繁更新,但不所有的更新都会影响到每个消费者组件时。通过选择器函数,它允许组件精确订阅并响应它们实际需要和关心的数据变化,从而避免过多的重新渲染,带来性能上的提升。

核心思想

在父组件中,通过MyContext.Provider提供Value值,在子组件中,通过useContextSelector进行消费。

需要注意的是:useContextSelector的第一个参数是MyContext上下文,因为在子组件中,可能会订阅多个上下文,第二个参数是一个函数,函数的入参就是Value,函数Value的哪个值(子组件)就监听指定的值,如果返回Value本身呢?

下面例子的妙用

import React, { FC, ReactNode, useState, useEffect, Dispatch, SetStateAction } from "react";
import { createContext, useContextSelector } from "use-context-selector";
import { getAllAssets } from "@/service/order";
import { ResCode } from "@/constant";
import { produce } from "immer";

type AssetsConfiguration = {
  supervision: number;
  report: number;
  conceptualization: number;
  transcription: number;
  speech: number;
  getData: () => void;
};

export const InitAssetsContextValue: AssetsConfiguration = {
  supervision: 0,
  report: 0,
  conceptualization: 0,
  transcription: 0,
  speech: 0,
  getData: () => {},
}

const AssetsConfigurationContext = createContext<[AssetsConfiguration, Dispatch<SetStateAction<AssetsConfiguration>>]|null>(null);

const AssetsStateProvider:FC<{ children: ReactNode }> = ({ children }) => {
  const [assetsSate, setAssetsSate] = useState(InitAssetsContextValue);

  const getData = async () => {
    try {
      const { code, data } = await getAllAssets();
      if(code === ResCode.Success) {
        setAssetsSate(produce(s => {
          s.supervision = data.supervision || 0;
          s.report = data.report || 0;
          s.conceptualization = data.conceptualization|| 0;
          s.transcription = data.transcription || 0;
          s.speech = data.speech || 0;
        }))
      }
    } catch (error) {
      console.log(error);
    }
  }

  useEffect(() => {
    setAssetsSate(produce(s => {
      s.getData = getData;
    }))
    getData();
  }, [])

  return (
    <AssetsConfigurationContext.Provider value={[assetsSate, setAssetsSate]}>{children}</AssetsConfigurationContext.Provider>
  );
}

// 子组件调用时,返回Value值,即:[assetsSate, setAssetsSate]
export const useAssetsSate = () => (useContextSelector(AssetsConfigurationContext, v => v) as [AssetsConfiguration, Dispatch<SetStateAction<AssetsConfiguration>>]);

export default AssetsStateProvider;

上面的useAssetsSate很好的思想:子组件调用时,返回上下文的Value值,即:[assetsSate, setAssetsSate]

比如在子组件中,更新对应的上下文值(指定的某个speech值):

setAssetsState(produce(s => {
   s.speech = s.speech - 1;
}))

需要注意的是,直接先获取服务端的最新值进行赋值!