笔记 redux + typescript

文章目录

  • 代码
  • 基础
    • Action
    • Reducer
    • Store
  • 高级
    • 异步 Action
    • Middleware
  • react-redux
    • 为项目添加 react-redux:
    • connect 高阶函数
    • Hook
      • useSelector
        • 比较和跟新:
        • 使用 memoizing selector
      • useDispatch()
      • useStore()

代码

本文完整例子链接 ,github 链接。

基础

redux 和 React 没有半毛钱关系。redux 是一个独立的数据管理方案。

Action

action 是对数据来源的封装,它被用来描述数据变化的信息,提供一组用来描述行为的数据。action 不是数据本身,也不是修改数据的行为,是行为的描述。

在 JS 里面描述一种行为很简单,因为行为实质是函数,只要辨别是使用了哪个函数,参数是什么,就行了。所以这也常常就是 Action 的数据结构:

// 伪代码
action = {
     
  type: 指定某个 action ,
  [... args : 执行行为所需的参数]
}

官方定义如下:

Action 是把数据从应用传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过 store.dispatch() 将 action 传到 store。

从 js 编程的方面来理解,action 是一个个对象,从 ts 的类型上来说,他们的组合就是辨识联合。

  • Action 创建函数是创建 Action 的方法
  • Action 不修改数据,只是提供行为的辨识和信息

Reducer

Reducers 指定了应用状态的变化如何响应 actions 并发送到 store 的,记住 actions 只是描述了有事情发生了这一事实,并没有描述应用如何更新 state。
一一官文中译

reducer 接收数据 State 和一个 action ,根据 Action 去改变数据 State 并返回。

(previousState, action) => newState

上述的函数就是 reducer。名字的由来是 Array.protorype.reduce ,都是接收旧状态返回新状态。reducer 被规定不能有以下操作:

  • 修改传入参数;
  • 执行有副作用的操作,如 API 请求和路由跳转;
  • 调用非纯函数,如 Date.now() 或 Math.random()。

只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。

Store

Action 描述行为;
Reducer 根据 Action 来更新 State;

那么 Store 就是把 Action 和 Reducer 联系起来的对象。Store 维持 State 状态,并且提供获取 State,分发 Action 到 Reducer,监听 State 变化的等功能。

Store 就是把它们联系到一起的对象。Store 有以下职责:

  • 维持应用的 state;
  • 提供 getState() 方法获取 state;
  • 提供 dispatch(action) 方法更新 state;
  • 通过 subscribe(listener) 注册监听器;
  • 通过 subscribe(listener) 返回的函数注销监听器。

尝试使用 redux:

import {
      createStore } from "redux";

// 数据类型
type State = Array<{
      data: string }>;
// Action 辨识
enum DISC {
     
  ADD = "LIST_ADD",
  UPDATE = "LIST_UPDATE",
}

interface Action_Add {
     
  type: DISC.ADD;
  data: string;
}
interface Action_Update {
     
  type: DISC.UPDATE;
  data: string;
  index: number;
}
type Actions = Action_Add | Action_Update;

// 添加数据的描述
export function listAdd(data: string): Action_Add {
     
  return {
      type: DISC.ADD, data };
}

// 修改数据的描述
export function listUpdate(data: string, index: number): Action_Update {
     
  return {
      type: DISC.UPDATE, data, index };
}

// 实例数据的 reducer
function list(state: State = [], action: Actions): State {
     
  switch (action.type) {
     
    case DISC.ADD:
      return [...state, {
      data: action.data }];

    case DISC.UPDATE:
      return state.map((val, index) => {
     
        return index === action.index ? {
      data: action.data } : val;
      });

    default:
      return state;
  }
}

// 创建一个 Store 实例
const store = createStore(list, [{
      data: "111" }]);

// 监听数据
const unLister = store.subscribe(() => {
     
  console.log(...store.getState());
});

store.dispatch(listAdd("something"));
store.dispatch(listAdd("something tow"));
store.dispatch(listAdd("something three"));

store.dispatch(listUpdate("aaaa", 1));
store.dispatch(listUpdate("bbbb", 2));
store.dispatch(listUpdate("cccc", 3));
store.dispatch(listUpdate("dddd", 4));

// 消除数据监听
unLister();

高级

异步 Action

写这段文章的时候我对国内的社区十分灰心,没有一个人或者一篇文章,好好介绍了异步 Action 结合typescript 的写法,因为官方文档和中译对此几乎都没介绍,国内社区全部是复制粘贴

最后能正确使用 ts ,是在redux-chunk 源码看到 ts 示例。

异步 Action 和同步的完全不一样,异步 Action 没有提供行为的基本信息,也没有提供行为的辨识。它不由 Reducer 来和数据做联系,而是被 thunk-redux 中间件直接处理。

异步 Action 一般长下面这个样子:

// es5
function asyncAction () {
     
	return function handle (dispatch , getState , extraArgument ) {
     
		return result = 'any result';
	}
}
// es6
asyncAction = () => (dispatch,getState,extraArgument) => result = 'any result';

异步 Action 并不是返回数据,甚至大多数情况下,异步 Action 都没有返回值或返回一个状态。

应用场景: 所有需要异步控制 State 的场景,都适合使用异步 Action。虽然不像普通 Action 一样提供行为描述的信息给 Reducer 操作,但是异步 Action 也是对行为的描述,不过是异步行为。

请求一组异步数据:

import {
      createStore, applyMiddleware } from "redux";
import thunk, {
      ThunkAction, ThunkMiddleware } from "redux-thunk";
import api from "../api";

// 数据类型
type State = Array<{
      data: string }>;

// Action 辨识
enum DISC {
     
  INIT = "LIST_INIT",
}

interface ActionInit {
     
  type: DISC.INIT;
  datas: State;
}

type Actions = ActionInit;
type ThunkReturn<R> = ThunkAction<R, State, unknown, Actions>;

// 修改数据的描述
export function listInit(datas: State): ActionInit {
     
  return {
      type: DISC.INIT, datas };
}

// 异步 Actions
export const asyncGetData = (): ThunkReturn<void> => dispatch => {
     
  api.getData().then((datas: State) => dispatch(listInit(datas)));
};

// 实例数据的 reducer
function list(state: State = [], action: Actions): State {
     
  switch (action.type) {
     
    case DISC.INIT:
      return [...action.datas];
    default:
      return state;
  }
}

// 创建一个 Store 实例
const store = createStore(
  list,
  applyMiddleware(thunk as ThunkMiddleware<State, Actions>)
);

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

// 分发异步actions
store.dispatch(asyncGetData());

标记下异步 Action 常用的类型:

// redux-think
// S:数据类型,E:额外参数,A:Action 类型
export interface ThunkDispatch<S, E, A extends Action> {
     
  <T extends A>(action: T): T;
  <R>(asyncAction: ThunkAction<R, S, E, A>): R;
}

// R:返回值类型,S:数据类型,E:额外参数类型,A:Action 类型
export type ThunkAction<R, S, E, A extends Action> = (
  dispatch: ThunkDispatch<S, E, A>,
  getState: () => S,
  extraArgument: E
) => R;

// S:数据类型,A:Action 类型,E:额外参数类型
export type ThunkMiddleware<S = {
     }, A extends Action = AnyAction, E = undefined> = Middleware<ThunkDispatch<S, E, A>, S, ThunkDispatch<S, E, A>>;

Middleware

同各大框架的中间件类似,redux 的中间件解决的是在分发行为后,行为执行前做的事。并且支持链式注册。

官方文档有 Middleware 的演变过程,一个 Middleware 定义如下:

/**
 * @template DispatchExt 为 Dispatch 提供额外的扩展签名.
 * @template S 中间件支持的 state 类型.
 * @template D 分发类型。
 */
interface Middleware<
  DispatchExt = {
     },
  S = any,
  D extends Dispatch = Dispatch
> {
     
  (api: MiddlewareAPI<D, S>): (
    next: Dispatch<AnyAction>
  ) => (action: any) => any
}

定义一个日记记录的的中间件并应用:

import {
      createStore, applyMiddleware, Middleware } from "redux";

// 数据类型
type State = Array<{
      data: string }>;
// Action 辨识
enum DISC {
     
  ADD = "LIST_ADD",
}

interface ActionAdd {
     
  type: DISC.ADD;
  data: string;
}
type Actions = ActionAdd;

// 修改数据的描述
export function listAdd(data: string): ActionAdd {
     
  return {
      type: DISC.ADD, data };
}

// 实例数据的 reducer
function list(state: State = [], action: Actions): State {
     
  switch (action.type) {
     
    case DISC.ADD:
      return [...state, {
      data: action.data }];
    default:
      return state;
  }
}

// 中间件
const logger: Middleware<{
     }, State> = store => next => action => {
     
  console.log("dispatching", action);
  let result = next(action);
  console.log("next state", store.getState());
  return result;
};

// 创建一个 Store 实例
const store = createStore(list, applyMiddleware(logger));

store.dispatch(listAdd("content"));

react-redux

react-redux 就是把 redux 放到了 react 中。具体的实现方式就是写了一个超级 Context,然后在应用的根部放入 Provider 组件,然后通过一个叫 connect 的高阶函数注入到需要使用 redux 数据的组件内。

为项目添加 react-redux:

import React from "react";
import TestComponent from "./components/TestComponent";
import {
      Provider } from "react-redux";
import store from "./store";

function App() {
     
  return (
    <Provider store={
     store}>
      <div className="App">
        <TestComponent />
      </div>
    </Provider>
  );
}

export default App;

就是那么简单就行了,不过事先要安装 react-redux:npm i -S react-redux
其中 store 就是上面一节中已经注册好了的。

connect 高阶函数

connect 被用来连接到 react-redux 的 Provider。connect 是一个高阶函数,返回一个高阶组件。

connect 可以通过传入需要一个回调设置到需要的状态,通过传入一堆 action 来设置被封装后的 action。这些设置会被封装到 connect 返回的高阶组件中,然后透过透传的方式,被升阶的组件可以直接从 props 里拿到这些状态和被封装的 action。

如果不想封装 action,可以不传入 action,那么可以在返回的高阶组件中 props 中拿到一个 props.diapatch 属性,用来自己分发 action。

connect一般用法如下:

// mapStateToProps 设置需要的状态,mapDispatchToProps 设置需要的分发的 action
connect(mapStateToProps,mapDispatchToProps)(Components);
// 有可能你不需要获取状态,那么第一个值可以传 null
connect(null,mapDispatchToProps)(Components);
// 也可能都不需要,自己通过 `props.dispatch` 分发
connect()(Components);

mapStateToProps 类型:

/**
 * 创建需要获取的状态
 */
const mapStateToProps = (state: RootState) => {
     
  return {
     
    student: state.student,
  };
};

这个简单,直接设置就行。

mapDispatchToProps 类型:

/**
 * 创建需要调用的 dispatch,绑定 action 创建函数。
 * 按照 props.action() 调用,但实际上执行了 dispatch(action)
 * @param dispatch 用以分发的 dispatch
 */
const mapDispatchToProps = (dispatch: MapDispatch) => ({
     
  /**
   * 普通 action 绑定可以通过快捷方式 bindActionCreators 创建,这段代码等价于
   * ```js
   * ...{
   *   addStudent:(name:string,grade:Grade) => dispatch(addStudent(name,grade)),
   *   updateStudent: (index: number, name: string, grade: Grade) => dispatch(index,name,grade),
   *   ......略
   * }
   * ```
   */
  ...bindActionCreators({
      addStudent, updateStudent, deleteStudent }, dispatch),

  /**
   * 虽然异步 action 也可以使用 bingActionCreators 创建,但是返回值类型会出问题,
   * 所以还是需要单独写,react-redux 和 thunk-redux 并不是完全兼容,
   */
  asyncGetStudent: () => dispatch(asyncGetStudent()),
});

有时不使用 mapDispatchToProps ,直接用 props.dispatch 来转发,那么在组件的 props 类型里还要加入一个 diapatch 类型。详见实例代码。

Hook

可以清楚的看到,在 react-redux 中,connect 是一个高阶函数,返回值是一个高阶组件。高阶组件有好有坏,这种问题使用 Hook 来解决会更方便些。

react-redux 迭代几个版本后,剩下 3 个核心的 hook:

  • useSelector:获取状态
  • useDispatch:获取分发
  • useStore:获取状态机

这三个 hook 使得使用 react-redux 更加简单:

useSelector

useSelector 提供对 redux 中状态的获取。
语法:

const result: any = useSelector(selector: Function, equalityFn?: Function)

useSelector 类似于 mapStateToProps,他们的参数存在一定的差异,这些差异来自 hook 的特性:

  • selector 可以自定义任何返回值,并把此返回值和 hook 的返回值绑定。
  • 在分发 action 的时候,根据 hook 特性,会比对新旧 hook 的返回值,并在不一样的时候重新渲染。
  • selector 没有 ownProps。但是,可以通过闭包或使用 curried selector 来使用 prop
  • 谨慎使用 memoizing selectors。
  • useSelector() 默认情况下使用严格的 === 检查,不是浅层比较。

比较和跟新:

mapState 中,因为状态存到了 connect 高阶函数返回的高阶组件中,所有状态被合并到对象中返回,返回的对象是否是新引用并没有多少关系,所以 connect 只是比较各个值。
使用 useSelector() ,因为 hook 的特性,每次默认返回一个新对象的话就会强制重新渲染。可以把对象拆分,多次使用 useSelector 减少渲染:

  • 尝试多次调用 useSelector
  • 使用 Reselect 或类似的库来创建 memoized selector,memoized selector 会在仅当对象中的值发生更改时才返回新对象。
  • 使用 react-redux 中的 shallowEqual 来作为 useSelector 的第二个参数:
import {
      shallowEqual, useSelector } from 'react-redux'

// later
const selectedData = useSelector(selectorReturningObject, shallowEqual)

使用 memoizing selector

memoizing selector 能有效减少计算的次数,memoizing selector 需要借助于库 reselect。这个东西类似于 Vue 里面的计算属性,专门针对计算的优化:
react-redux 官网示例(不是 TSX):

import React from 'react'
import {
      useSelector } from 'react-redux'
import {
      createSelector } from 'reselect'

/**
 * createSelector(... inputSelectors | [inputSelectors],resultFunc)
 * 至少2个参数
 * inputSelectors 是函数,其参数有 createSelector 的返回值提供。作用是对状态的获取,返回值将依次传递给 resultFunc
 * resulFunc 对状态进行计算并返回值
 * 返回值:返回一个处理后的 selector,参数将传递给 inputSelectors,结果由 resultFunc 提供
 */
const selectNumOfTodosWithIsDoneValue = createSelector(
  state => state.todos,
  (_, isDone) => isDone,
  (todos, isDone) => todos.filter(todo => todo.isDone === isDone).length
)

export const TodoCounterForIsDoneValue = ({
      isDone }) => {
     
  const NumOfTodosWithIsDoneValue = useSelector(state =>
    selectNumOfTodosWithIsDoneValue(state, isDone)
  )

  return <div>{
     NumOfTodosWithIsDoneValue}</div>
}

export const App = () => {
     
  return (
    <>
      <span>Number of done todos:</span>
      <TodoCounterForIsDoneValue isDone={
     true} />
    </>
  )
}

useDispatch()

useDispatch 就是单纯的获取到分发器,从而分发各个行为。没有参数,直接调用即可。

useStore()

useStore() 获取状态机。没有参数,直接调用。

参考链接:

  • 《Typescript with Redux, & Redux-Thunk Recipe》
  • Dispatching a Redux Thunk action outside of a React component with TypeScript
  • Redux Thunk with Typescript
  • Redux + TypeScript and mapDispatchToProps
  • How to dispatch an Action or a ThunkAction (in TypeScript, with redux-thunk)?

你可能感兴趣的:(笔记,react,typescript,react)