TypeScript下使用Hooks的方式重新学习Redux和React-Redux

使用过vuex的同学肯定了解vuex无论是集成难度,还是上手难度,都远小于React-Redux,目前要实现vuex同样的功能则需要3个库,分别是Redux、React-Redux、Redux-Saga,它们分别负责下面几种工作。

  • Redux:Redux是JavaScript状态容器,提供可预测化的状态管理。
  • React-Redux:React官方提供的将React项目与Redux项目绑定的库。
  • Redux-Saga:一个用于管理应用程序 Side Effect(副作用,例如异步获取数据,访问浏览器缓存等)的library,它的目标是让副作用管理更容易,执行更高效,测试更简单,在处理故障时更容易。

然而由于Redux-Saga的复杂性,本篇文章就先暂且不谈,仅仅谈论一下Redux、React-Redux,也正是因为这一系列的库上手成本比较高,于是由蚂蚁金服推出了一个整合库dva,它整合了这3个库,同时还内置了React-router和fetch,但是从普及程度上面考虑,Redux、React-Redux还是不得不学习一下的。

那么什么时候该使用Redux,我的想法是:当你觉得组件之间的通信让你头痛的时候,你就可以使用Redux。

尤其是在一个界面有非常多的表单需要填写,填写完毕后需要将数据统一起来发送给后端,因为你不可能不将表单分割成为一个一个组件,一旦表单组件被分割,里面的数据传递就会非常复杂。当然还有一种解决方式是不分割组件,全部写在一个组件中(我还真见过这种表单组件,没格式化之前4000行代码,一格式化超过1W行,而且几乎没有注释,你根本不知道有些代码是在干什么,直接无法维护…)。

和Vue一样,在一个比较大的项目中或者一个通信比较复杂的项目中,要实现兄弟组件、爷孙组件的通信是一件非常麻烦和困难的事情(虽然我最近了解到React中有Context),为了解决这个问题,React项目中就引入了ReduxReact-Redux,值得注意的是,Redux不光可以使用在React项目中,在其它框架的项目中也是可以进行使用。

我们先来尝试一下Redux:

1. 使用Redux

Redux的使用一共分为下面几个步骤:

  1. 创建state。
  2. 创建action。
  3. 通过dispatch进行通信。

当然这是我总结的感觉语言并不官方…下面就来分别看一下这3个步骤吧!

1.1 创建state

因为我特别喜欢TypeScript,所以下面的代码都是使用的TypeScript。

interface action {
  type: "INCREMENT" | "DECREMENT";
}

const counter = (state = 0, action: action) => {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;
    case "DECREMENT":
      return state - 1;
    default: // 不要忘记默认返回值,不然再创建时会报错
      return state;
  }
};

1.2 创建Store

const store = createStore(counter);

store.subscribe(() => console.log(store.getState())); // 当store里面的属性改变时,触发的回调

一般来讲是不需要store.subscribe(() => console.log(store.getState()));这条语句的,但是为了下面我们进行通信的时候能够清楚的看到store中的变化,所以我们添加上这条语句。

1.3 通信

store.dispatch({ type: "DECREMENT" }); // 触发state + 1
store.dispatch({ type: "INCREMENT" }); // 触发state - 1

其中可以将{ type: "DECREMENT" }定义为一个函数。

const increment = (): action => ({
  type: "INCREMENT",
});

const decrement = (): action => ({
  type: "DECREMENT",
});

store.dispatch(increment());
store.dispatch(decrement());

对于字符串"INCREMENT",推荐定义为一个常量,因为毕竟{ type: "INCREMENT" }中的"INCREMENT"字符串书写的时候没有提示,非常容易写错,不过用了TypeScript的话是会有提示的,这也是TypeScript比较赞的一点。

如果一旦写错,在TypeScript中还会报错:

image-20210311230717430

所以在JavaScript中因为"INCREMENT"这一类的字符串书写没有提示的关系,所以很多人会为了防止写错,而给它定义一个常量(当然TypeScript中也推荐这样做)。

const INCREMENT_TYPE = "INCREMENT";  // 定义常量

const increment = (): action => ({
  type: INCREMENT_TYPE, // 使用常量进行调用
});

store.dispatch(increment());

一旦定义常量后,就会获得代码提示,也就不是那么容易会写错。

1.4 传值

store.dispatch()还可以传入一个参数,一般我们定义参数名称为payload或者data

interface action {
  type: "INCREMENT" | "DECREMENT";
  payload?: number;    // 传入参数一般命名为 payload 或者 data
}

const INCREMENT_TYPE = "INCREMENT";

const increment = (nr: number): action => ({
  type: INCREMENT_TYPE,
  payload: nr,
});

const decrement = (): action => ({
  type: "DECREMENT",
});

const counter = (state = 0, action: action) => {
  switch (action.type) {
    case "INCREMENT":
      return state + (action.payload as number);
    case "DECREMENT":
      return state - 1;
    default:
      return state;
  }
};

const store = createStore(counter);

store.subscribe(() => console.log(store.getState()));

store.dispatch(increment(5));
store.dispatch(decrement());

2. React-Redux

React Hooks的出现让React的易用性升到了另一个等级,我记得我曾经用React的class组件时,需要进行操作的步骤还挺多,而现在React-Redux提供了两个hook,使用这两个hook就可以轻松的访问Store中的状态和触发其中的行为。

由于是项目工程文件,肯定是要进行模块化的,虽然上面的那种写法也是可以,但是会给后期维护带来很大的困难,目前我的习惯是分成:

image-20210312160355180

modules中就放和state相关的东西,比如下面这段代码,就放进modules文件夹下:

const counter = (state = 0, action: action) => {
  switch (action.type) {
    case "INCREMENT":
      return state + (action.payload as number);
    case "DECREMENT":
      return state - 1;
    default:
      return state;
  }
};

actions就放触发事件相关的代码,比如下面的这些代码就放入actions文件夹下面的文件中:

export interface action {
  type: "INCREMENT" | "DECREMENT";
  payload?: number;
}

const INCREMENT_TYPE = "INCREMENT";

export const increment = (nr: number): action => ({
  type: INCREMENT_TYPE,
  payload: nr,
});

export const decrement = (): action => ({
  type: "DECREMENT",
});

而store下的index.ts文件就将上面所有的文件整合成为一个Store,并且导出,同时还可以添加Redux调试工具,关于Redux调试工具具体信息可以点击查看:

import { combineReducers, createStore } from "redux";
import counter from "./modules/counter";
import isLogged from "./modules/isLogged";

// 整合
const allReducers = combineReducers({ counter, isLogged });

// 注册
const store = createStore(
  allReducers,
  // @ts-ignore
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() // 引入Redux调试工具
);

// 导出
export default store;

最后一步就是我们需要引入并且在项目中使用React-Redux。

import { Provider } from "React-Redux";
import store from "./store";

ReactDOM.render(
  <Provider store={store}>
    <React.StrictMode>
      <App />
    </React.StrictMode>
  </Provider>,
  document.getElementById("root")
);

上面的Provider是一个顶层组件,通过它就可以注册一个所有组件都可以访问的store。

到这里为止,我们已经在React项目中注册了store,接下来我们可以随时在需要的时候在组件中访问store中的数据。

需要使用到两个hook,其它的Hook可以参考文章最后的参考链接:

  • useSelector:从Redux存储状态中提取数据。
  • useDispatch:该hook会返回一个dispatch函数,通过dispatch函数就可以触发actions。

2.1 在组件中调用

知道上面的那两个hook后就可以开始搞事情了。

import React from "react";
import "./App.css";
import { useDispatch, useSelector } from "react-redux";
import { increment } from "./store/actions";

interface AllState {
  counter: number;
  isLogged: boolean;
}

function App() {
  // useSelector声明了一个泛型,第一个是state的类型,第二表示返回值的类型
  // 即下面的代码counter的类型是number
  const counter = useSelector<AllState, AllState["counter"]>(
    (state) => state.counter
  );
  const isLogged = useSelector<AllState, AllState["isLogged"]>(
    (state) => state.isLogged
  );
  // 调用useDispatch函数会返回一个dispatch函数
  const dispatch = useDispatch();
  return (
    <div className="App">
      {counter}
      {isLogged ? "1" : "2"}
      <button onClick={() => dispatch(increment(5))}>点我</button>
    </div>
  );
}

export default App;

因为方便代码演示,所以接口AllState直接在组件中声明,在一般情况下还是推荐声明在index.ts中。

2.2 解决type重复

如果你的项目够大,你就会拥有很多的action,那么action type就有可能会出现重复的情况,这就会造成一些BUG的产生,我所知道的解决的方法有两种:

一种是引入命名空间,即在字符串前面加上xxx/这种写法,vuex就是使用的这种方法。

export interface action {
  type: "a/INCREMENT";
  payload?: number;
}

export const INCREMENT_TYPE = "a/INCREMENT"; // 引入命名空间

export const increment = (nr: number): action => ({
  type: INCREMENT_TYPE,
  payload: nr,
});

一种是使用symbol:

export interface action {
  type: symbol;
  payload?: number;
}

export const INCREMENT_TYPE = Symbol("INCREMENT");

export const increment = (nr: number): action => ({
  type: INCREMENT_TYPE,
  payload: nr,
});

这两种方法都可以解决type有可能重复的问题。

3. 最后

到目前为止,你就可以愉快的在项目中使用Redux了,但是还存在一个缺陷,就是无法处理异步请求,如果需要处理异步请求则需要使用Redux-Saga或者Redux-thunk。

这篇文章所讲述的内容其实都非常基础,但是我看到Redux、React-Redux中还有其它的一些功能,因为我暂且没有使用到所以就暂时不进行讲解了(其实是我不会),顺便说一下,更新了hooks的React-Redux是真的非常好用,因为我最开始学习React的时候教程中还是使用的class,当时就觉得对比vuex来说有非常多的不方便的地方。

参考资料:

React-Redux Hooks