likes
comments
collection
share

React Context 最佳实践探索

作者站长头像
站长
· 阅读数 57

React Context 的基本使用

  1. 使用 const TestContext = createContext(defaultVal) 创建一个 Context;
  2. 在祖先组件所在位置 使用 <TestContext.Provider value = {透传的内容}> 祖先组件 </ TestContext.Provider>包裹组件组件,并提供想要透传给 后代组件访问的内容。
  3. 在 后代组件当中使用 const ctxValue = useContext(TestContext) 获取该 context 传递的内容。

React Context 使用场景

登陆后的个人信息往往会在很多不同层级的组件当中使用,可以通过在跟组件的外层包裹一层 Context.Provider 并将用户信息提供出去,方便的让各个子组件都能方便的获取到用户登陆后的个人信息。

我们还希望其他组件可以方便的更新 Context.Provider 提供的用户 state 状态,当用户信息的某些状态发生改变时,希望子组件当中用到该用户信息状态的组件根据最新的 provider value 重新渲染,这通常需要 Context 与 useState 状态结合使用。

基于以上场景,为了更加方便的的使用及复用 Conext 的逻辑,尝试封装一个创建并使用 Context 的工具,让开发者通过尽可能少的代码实现上述功能。

React 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 组件当中。

  1. 调用工厂函数,创建唯一标识 key 所对应的 Context,返回值为一个高阶函数,其接收一个要被包裹的组件参数。
  2. 调用工厂返回的高阶函数,传入一个要被包裹的组件参数,返回值是一个被 Provider 包裹好的函数组件。
  3. 使用高阶函数返回的函数组件进行渲染。
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
评论
请登录