You Might Not Need Redux.
—— Dan Abramov
但是我们可以用 useReducer 和 useContext ~
前面说的话:
useContext 可以实现状态共享,useReducer 可以实现犹如 redux 状态管理器 dispatch 的功能 。
这样一来,我们就可以拿这两个hook来实现一个简单的状态管理器了。
如果再加入ts呢,我们可以想到的是自己定义的诸多 type
,通过ts加编辑器的支持,在我们眼前呈现的那种愉悦感 ~
在项目中,我们可能会有多个状态需要共享,我们可能会在登录后异步请求获取用户信息,然后在其他页面会用到这个用户信息… 。
那就让我们就用这两个hook举个例子吧:
( 这两个hook还不了解的小伙伴,可以看上一篇文章介绍,点我点我 )
实现异步获取用户信息的相关文件
userInfo/index.declare.ts
export interface IState { id?: string; name?: string; isFetching?: boolean; failure?: boolean; message?: string; } type TType = | "ASYNC_SET_USER_INFO" | "FETCHING_START" | "FETCHING_DONE" | "FETCHING_FAILURE"; export interface IAction { type: TType; payload?: IState; }
这个文件这里把它提取出来,声明了基本的 state 、type的约束与action,参数用 payload 来接收
userInfo/index.tsx
import React, { useReducer, createContext, useContext } from "react"; import { IState, IAction } from "./index.declare"; // 初始化状态 const initialState: IState = { id: "", name: "", isFetching: false, failure: false, message: "" }; // 创建一个 context,并初始化值 const context: React.Context<{ state: IState; dispatch?: React.Dispatch<IAction>; }> = createContext({ state: initialState }); // reducer const reducer: React.Reducer<IState, IAction> = ( state, { type, payload } ): IState => { switch (type) { case "ASYNC_SET_USER_INFO": { const { id, name, message } = payload!; return { ...state, id, name, message }; } case "FETCHING_START": { return { ...state, failure: false, isFetching: true }; } case "FETCHING_DONE": { return { ...state, isFetching: false }; } case "FETCHING_FAILURE": { return { id: "", name: "", failure: true, message: payload?.message }; } default: throw new Error(); } }; /** * mock:模拟了请求接口的异步等待 */ const request = (id: string): Promise<any> => { return new Promise((resolve, reject) => { setTimeout(() => { if (id === "998") { resolve({ id: "998", name: "liming", message: "获取用户成功" }); } else { reject(`找不到id为${id}的用户`); } }, 1000); }); }; /** * dispatch 异步/同步 高阶函数 */ const dispatchHO = (dispatch: React.Dispatch<IAction>) => { return async ({ type, payload }: IAction) => { if (type.indexOf("ASYNC") !== -1) { dispatch({ type: "FETCHING_START" }); try { const { id, name, message } = await request(payload!.id!); dispatch({ type, payload: { id, name, message } }); } catch (err) { dispatch({ type: "FETCHING_FAILURE", payload: { message: err } }); } dispatch({ type: "FETCHING_DONE" }); } else { dispatch({ type, payload }); } }; }; /** * ProviderHOC 高阶组件 */ export const ProviderHOC = (WrappedComponent: React.FC) => { const Comp: React.FC = (props) => { const [state, dispatch] = useReducer(reducer, initialState); return ( <context.Provider value={{ state, dispatch: dispatchHO(dispatch) }}> <WrappedComponent {...props} /> </context.Provider> ); }; return Comp; }; /** * 封装 useContext */ export const useContextAlias = () => { const { state, dispatch } = useContext(context); return [state, dispatch] as [IState, React.Dispatch<IAction>]; };
解释:
- request 方法只是一个模拟接口请求等待而已。
- 在真实的业务场景中,我们会异步请求用户信息,所以实现 异步action 的核心代码就在 dispatchHO 方法,这是一个高阶函数,dispatch 作为参数。在我们发起异步请求时,我们需要对一些状态进行改变,如请求前,请求成功,请求失败…,我们需要把它封装到一个 “大dispatch” 里。约定 type 有 “ASYNC” 的情况下才会触发这个特别的 “大dispatch”。
- ProviderHOC 是一个高阶组件,一般我们会用这种写法,来共享状态,如:
<context.Provider value={obj}> <App /> </context.Provider>;
但是这里我们用高阶组件的方式,让我们在对 root 组件包裹的时候可以更灵活,请耐心继续往下看。
- useContextAlias方法 是对 useContext 的再封装,这里我把它转换成我们比较了解的 useReducer 写法,如:
const [state, dispatch] = useContext();
项目中文件目录结构可能是这样子的
我们可以看到 reducers 专门用来放一些 reducer 模块,userInfo、userList…
reducers/index.ts 作为一个 main 文件,我们来看看里面的实现:
import React from "react"; import { ProviderHOC as ProviderHOCUserList, useContextAlias as useContextUserList } from "./userList"; import { ProviderHOC as ProviderHOCUserInfo, useContextAlias as useContextUserInfo } from "./userInfo"; /** * 组合各个 provider */ const compose = (...providers: any[]) => (root: any) => providers.reverse().reduce((prev, next) => next(prev), root); const arr = [ProviderHOCUserList, ProviderHOCUserInfo]; const providers = (root: React.FC) => compose(...arr)(root); export { useContextUserList, useContextUserInfo }; export default providers;
解释:
- compose 方法是组合各个 provider 的核心方法,我们引入了各个模块暴露出来的方法 ProviderHOC 然后再进行组合他们,这使得我们可以很灵活的去添加更多的 provider ,而不必要手动的在 root 组件上进行包裹,在App中我们就可以这样,如:
App.tsx
import React from "react"; import "./styles.css"; import providers from "./reducers"; import UseReducerDemo from "./userReducer.demo"; const App = () => { return ( <div className="App"> <UseReducerDemo /> </div> ); }; export default providers(App);
- 我们把 import 进来的 useContextUserList, useContextUserInfo,别名之后再次导出,在其他页面,只要针对的引入想要用的 context 即可,如:
userReducer.demo.tsx
import React from "react"; import { useContextUserInfo, useContextUserList } from "./reducers"; const Index: React.FC = () => { const [userInfo, dispatchUserInfo] = useContextUserInfo(); const [userList, dispatchUserList] = useContextUserList(); return ( <div className="demo"> userInfo: <p>状态:{userInfo.isFetching ? "正在加载中..." : "加载完毕"}</p> <p>id:{userInfo.id}</p> <p>name:{userInfo.name}</p> <p>message:{userInfo.message}</p> <button disabled={userInfo.isFetching} onClick={() => { dispatchUserInfo({ type: "ASYNC_SET_USER_INFO", payload: { id: "998" } }); }} > 异步获取用户信息 id="998" </button> <button disabled={userInfo.isFetching} onClick={() => { dispatchUserInfo({ type: "ASYNC_SET_USER_INFO", payload: { id: "1" } }); }} > 异步获取用户信息 id="1" </button> ); }; export default Index;
总结
我们在做项目的时候,这两个hook可以用来做很轻巧的 redux,我们还可以自己实现异步 action。再加上ts,让我们在其他页面书写 dispatch 有一种稳重感 ~,用 compose 方法组合各个高阶组件,让我们更加灵活的共享各个状态。
所以,赶紧点我查看完整例子