useContextSelector 是一个 React 钩子(hook)来自于 use-context-selector 库,这个库提供了一种方法来优化性能,通过避免在 React 的 Context API 的消费者组件中在上下文值未发生变化的情况下重新渲染。
使用 useContextSelector 的步骤如下:
-
安装
use-context-selector库:
如果你还没有安装这个库,你可以使用 npm 或 yarn 来安装它:npm install use-context-selector # 或者使用 yarn yarn add use-context-selector -
创建上下文 (Context) :
首先,你需要创建一个上下文,通常这是全局完成的,以便在整个组件树中可用。import { createContext } from 'use-context-selector'; const MyContext = createContext(null); -
提供上下文值 (Context Provider) :
在组件树中的某个高层级位置,使用MyContext.Provider来提供上下文数据。<MyContext.Provider value={/* 上下文值,比如 state 对象 */}> {/* 应用的其他部分 */} </MyContext.Provider> -
消费上下文中的特定值:
在需要订阅上下文中特定部分数据的组件内,使用useContextSelector。import { useContextSelector } from 'use-context-selector'; const myValue = useContextSelector(MyContext, context => context.partOfContext);MyContext是你创建的上下文对象。- 第二个参数是一个选择器函数,这个函数接收上下文值作为参数,并返回这个上下文中你感兴趣的那部分数据。
-
更新上下文中的值:
如果上下文中的某些数据需要更新,可以提供一个函数作为上下文的一部分,然后在子组件中通过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;
}))
需要注意的是,直接先获取服务端的最新值进行赋值!