React Context 最佳实践探索
React Context 的基本使用
- 使用
const TestContext = createContext(defaultVal)
创建一个 Context; - 在祖先组件所在位置 使用
<TestContext.Provider value = {透传的内容}> 祖先组件 </ TestContext.Provider>
包裹组件组件,并提供想要透传给 后代组件访问的内容。 - 在 后代组件当中使用
const ctxValue = useContext(TestContext)
获取该 context 传递的内容。
React Context 使用场景
登陆后的个人信息往往会在很多不同层级的组件当中使用,可以通过在跟组件的外层包裹一层 Context.Provider 并将用户信息提供出去,方便的让各个子组件都能方便的获取到用户登陆后的个人信息。
我们还希望其他组件可以方便的更新 Context.Provider 提供的用户 state 状态,当用户信息的某些状态发生改变时,希望子组件当中用到该用户信息状态的组件根据最新的 provider value 重新渲染,这通常需要 Context 与 useState 状态结合使用。
基于以上场景,为了更加方便的的使用及复用 Conext 的逻辑,尝试封装一个创建并使用 Context 的工具,让开发者通过尽可能少的代码实现上述功能。
Context 封装实现
1、创建 Context 基本内容的类
创建一个涵盖 Context 核心内容的类,其接收一个 key 与一个 Context 默认值 defaultValue,我们会基于这个类去扩展其他相关方法,根据创建时提供的唯一 key 区分不同的 Context。
import React, {
createContext, useMemo, useState, useContext,
} from 'react';
// 缓存并集中管理 Context
const cxtCache: Record<string, Cxt> = {};
class Cxt<T = any> {
defaultStore: IStore<T>;
AppContext: React.Context<IStore<T>>;
Provider: ({ children }: IPropChild) => JSX.Element;
// 实例化 Context 类需要传递一个唯一标识 key 和默认值 defaultValue
constructor(key: string, defaultValue: T) {
// 提供的值包括该 Context 的标识 key,Store数据、修改Store的方法
this.defaultStore = {
key,
store: defaultValue,
setStore: () => {},
};
// 借助 createContext 创建 Context
this.AppContext = createContext(this.defaultStore);
// 提供 Context Provider
this.Provider = getCxtProvider(key, defaultValue, this.AppContext);
// 根据 Context 的标识 key 缓存这个 Context对象
cxtCache[key] = this;
}
}
对于上方 Cxt 类中的 this.Provider 属性值,其内容是被 Context.Provider 包裹好的函数组件,可以使用它直接包裹子组件。
所以要额外封装一个 getCxtProvider 方法,用于初始化 Store 数据和修改 Store 的方法,并将其作为 Provider 所透传的内容。
import React, {
createContext, useMemo, useState, useContext,
} from 'react';
function getCxtProvider<T>(
key:string,
defaultValue: T,
AppContext: React.Context<IStore<T>>,
) {
return ({ children }: IPropChild) => {
// 将传递的默认值变成响应式的
const [store, setStore] = useState(defaultValue);
// 汇总要被 Provider 透传的内容
const value = useMemo(() => ({
key,
store,
setStore: (payload = {}) => setStore((state) => ({
...state,
...payload,
})),
}), [store]);
return (
<AppContext.Provider value={value}>
{children}
</AppContext.Provider>
);
};
}
2、抽离 useContext 的使用
封装 useContext 方法,通过 key 获取到初始化好的 Context 实力对象,获取到其通过 Provider 传递下来的内容。
由于暴露出来了 setStore 方法用于更改默认值 Store,所以可以在获取后端数据的请求函数中通过 setStore 方法将返回值回填回去。
import React, {
createContext, useMemo, useState, useContext,
} from 'react';
function useAppContext<T> = (key: string) => {
const Ctx = cxtCache[key] as Ctx<T>;
const CtxValue = useContext(Ctx.AppContext);
return {
store: CtxValue.store,
setStore: CtxValue.setStore
}
}
3、创建 Context 工厂
通过单例模式保证 Context 全局唯一,返回一个高阶组件,接收 需要被提供 Provider 的组件作为参数,将其包裹在 Context.Provider 组件当中。
- 调用工厂函数,创建唯一标识 key 所对应的 Context,返回值为一个高阶函数,其接收一个要被包裹的组件参数。
- 调用工厂返回的高阶函数,传入一个要被包裹的组件参数,返回值是一个被 Provider 包裹好的函数组件。
- 使用高阶函数返回的函数组件进行渲染。
function connectFactory<T>(key:string, defaultVal: any) {
const ctx = ctxCache[key];
let CurCxt: Cxt<T>;
if (cxt) {
CurCxt = cxt;
} else {
CurCxt = new Cxt<T>(key, defaultValue);
}
return (Child: React.FunctionComponent<any>) => (props: any) => (
<CurCxt.Provider>
<Child {...props} />
</CurCxt.Provider>
);
}
上面返回的高阶函数我个人觉得有些麻烦,不如直接返回一个函数组件,变成下面这样。 下面的示例用的是上方代码返回的高阶函数编写的。
function connectFactory<T>(key:string, defaultVal: any) {
const ctx = ctxCache[key];
let CurCxt: Cxt<T>;
if (cxt) {
CurCxt = cxt;
} else {
CurCxt = new Cxt<T>(key, defaultValue);
}
return ({children}) => (
<CurCxt.Provider>
{children}
</CurCxt.Provider>
);
}
这样直接套用感觉更容易使用,处理 props 的逻辑都放到对应被包裹的组件中去处理,Context 工厂函数的返回值仅仅处理把传递进来的组件包裹着一件事
使用示例:
在单独的 hooks 文件中管理用户信息的 context,在该文件中完成用户信息Context的创建、初始化、数据请求与回填等操作。
import { useQuery } from '@apollo/client';
import { useLocation, useNavigate } from 'react-router-dom';
import { useAppContext, connectFactory } from '../utils/contextFactory';
import { GET_USER } from '../graphql/user';
import { IUser } from '../utils/types';
const KEY = 'userInfo';
const DEFAULT_VALUE = {
};
export const useUserContext = () => useAppContext<IUser>(KEY);
// 根据标识 key 创建对应的 Context 实例
// 返回值是一个接收 要被包裹的组件为参数的 高阶组件函数
export const connect = connectFactory(KEY, DEFAULT_VALUE);
// 1. 通过 useContext 获取到 setStore
// 2. 使用 setStore 将接口的返回值回填回 Context Store 当中
// 使得 Context 透传的值 与接口返回值绑定,且可以灵活的改变
export const useGetUser = () => {
const { setStore } = useUserContext();
const nav = useNavigate();
const location = useLocation();
const { loading, refetch } = useQuery<{ getUserInfo: IUser }>(GET_USER, {
onCompleted: (data) => {
if (data.getUserInfo) {
const {
id, name, tel, desc, avatar,
} = data.getUserInfo;
setStore({
id, name, tel, desc, avatar, refetchHandler: refetch,
});
// 当前在登录页面,且已经登录了,那就直接跳到首页
if (location.pathname === '/login') {
nav('/');
}
return;
}
setStore({ refetchHandler: refetch });
// 如果不在登录页面,但是目前没有登录,那就直接跳到登录页面
if (location.pathname !== '/login') {
nav(`/login?orgUrl=${location.pathname}`);
}
},
onError: () => {
setStore({ refetchHandler: refetch });
// 如果不在登录页面,但是目前登录异常,那就直接跳到登录页面
if (location.pathname !== '/login') {
nav(`/login?orgUrl=${location.pathname}`);
}
},
});
return { loading };
};
在要被包裹的父组件当中调用上面封装的请求数据并回填 Context Store 的方法,让 Context Provider 提供的值是请求到的用户数据。
将该组件作为参数传递给 Context 工厂函数返回的高阶函数。
import { IPropChild } from '@/utils/types';
import { connect, useGetUser } from '@/hooks/userHooks';
import { Spin } from 'antd';
/**
* 获取用户信息组件
*/
const UserInfo = ({ children }: IPropChild) => {
// 调用封装的发送请求并回填用户信息 Context Store 的方法
// 此时 Provider 的就是请求回来的用户数据了
const { loading } = useGetUser();
return (
<Spin spinning={loading}>
<div style={{ height: '100vh' }}>
{children}
</div>
</Spin>
);
};
// 将组件作为参数传递给 Context 工厂函数返回的高阶函数,使其被 Context.Provider 包裹
export default connect(UserInfo);
在根组件当中使用 connect(UserInfo)
所返回的函数组件。
// 引入 connect(UserInfo) 返回的函数组件
import UserInfo from './components/UserInfo';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<ApolloProvider client={client}>
<BrowserRouter>
{/* 使用 Context.Provider 包裹其余子组件 */}
<UserInfo>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/" element={<Layout />}>
{routes.map((item) => {
const Component = ROUTE_COMPONENT[item.key];
return (
<Route
path={item.path}
key={item.key}
element={<Component />}
/>
);
})}
</Route>
</Routes>
</UserInfo>
</BrowserRouter>
</ApolloProvider>,
);
转载自:https://juejin.cn/post/7375345204045299712