React Hook丨这两个hook加ts,如虎添翼

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 方法组合各个高阶组件,让我们更加灵活的共享各个状态。

所以,赶紧点我查看完整例子

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,294评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,780评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,001评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,593评论 1 289
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,687评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,679评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,667评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,426评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,872评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,180评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,346评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,019评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,658评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,268评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,495评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,275评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,207评论 2 352

推荐阅读更多精彩内容