li-jia-nan / my-blog

个人技术博客,同步掘金,文章写在 Issues 里
43 stars 1 forks source link

Toolkit学习笔记:如何用Toolkit改造你的Redux #11

Open li-jia-nan opened 1 year ago

li-jia-nan commented 1 year ago

一、前言

在一般的React项目中,大多数情况会选择Redux或者React-Redux作为我们的状态管理工具,在使用Vuex的时候,我们可以在mutations里写同步操作,也可以在actions里写异步操作,然而Redux不同于VuexRedux本身是不支持异步的,如果需要处理异步操作,我们还要额外安装redux-thunk或者redux-saga这样的中间件,显得很繁琐。

如果你的React项目中使用了react hookreduxredux-thunk、或者redux-saga,那么可能你需要用redux-toolkit(以下简称RTK)来优化你的项目结构,它可以让你的代码看起来更清爽。

# 优化前
  /counter
    constants.ts
    actions.ts
    reducer.ts
    saga.ts
    index.tsx

# 优化后
  /counter
    slice.ts
    index.tsx

二、简介

RTK旨在帮助解决关于Redux的几个问题:

简单讲,配置Redux的流程太过复杂,完整需要编写actionTypes、actions、reducer、store等一系列函数,最后通过connect隐射到props里面供组件使用。而使用RTK,只需一个reducer即可。

那么RTK到底是什么呢?

三、核心依赖

我们打开GitHub,找到RTK的源码,可以在 toolkit/package.json目录中看到关于RTK用到的一些依赖:

111.jpg

关于这些依赖都代表什么呢?

第一个: redux

当然了,RTK需要它才能工作,这里引入了redux,意味着如果我们的项目安装了RTK,就不需要重复安装redux;除此之外,如果大家观察的足够仔细,就会发现RTK的依赖并不包含react-redux,这意味着,RTK是一个独立的库,它不只是给react项目使用,如果你愿意,你可以在任何环境中使用它,比如Vue或者Angular,甚至jQuery中,或者原生js中都可以使用。

第二个: redux-thunk

RTK自带了redux-thunk来处理异步逻辑,thunk在RTK中是默认开启的(在开发过程中,你可以手动关闭,如果你愿意,也可以安装redux-saga等其它异步处理的中间件)。

第三个: reselect

这也是一个比较流行的redux插件,它可以帮助我们在视图渲染的时候记住当前的状态,防止组件在不需要的时候被无意识的渲染,功能有点类似于shouldComponentDidUpdate,但他们并不是一个东西,这里作为RTK入门笔记,对该插件不做深入讲解。

第四个: immer

最后一个immer是个非常有意思的插件,它允许我们把state的immutable特性转化为mutable,也就是说,我们在reducer函数中,可以直接修改state中的数据(我在第一次听到这个思路的时候,理智告诉我,这是不对的,因为这样就违反了reducer函数式编程的理念,redux是单向数据流的状态管理工具,数据是不可变的,我们不可以直接修改store的状态,而是通过返回新state,替换旧state来完成状态更新,所以我一开始接触到这个概念的时候,还是比较抵触的)。总而言之,我们可以选择使用immer来修改state,让数据变成mutable状态,也可以选择不使用immer,让数据保持immutable状态,这完全取决于你自己,大家可以自行决定。(这不正是React的灵活之处吗)

四、关于immer库

在上文中,我们对immer做了简单的介绍,这里再单独拿出来讨论一下

immer到底是什么?在上文中我们提到,它是一个很有意思的插件,它允许我们把state的immutable特性转化为mutable,实际上,immer在底层是的核心实现是利用了ES6的proxy,在我们对状态进行修改的时候,proxy对象进行拦截,并且proxy按顺序替换上层对象,相当于自动帮你返回的新对象(所以其实还是immutable的,只不过写法上看起来是可直接修改state)

下面的例子是用immer和不用immer的区别:

// 不使用 immer,返回新状态,替换旧状态
reducers: {
  fetchStart(state) {
    return { ...state, loading: true };
  },
  fetchEnd(state) {
    return { ...state, loading: false };
  },
  fetchSuccess(state, action: PayloadAction<FetchType>) {
    return { ...state, data: action.data };
  },
  fetchFailure(state, action: PayloadAction<ErrorType>) {
    return { ...state, error: action.message };
  },
},

// 使用 immer,无需返回新状态,直接修改原状态
reducers: {
  fetchStart(state) {
    state.loading = true;
  },
  fetchEnd(state) {
    state.loading = false;
  },
  fetchSuccess(state, action: PayloadAction<FetchType>) {
    state.data = action.data;
  },
  fetchFailure(state, action: PayloadAction<ErrorType>) {
    state.error = action.message
  },
},

于是有同学会问了,在上面的例子中,使用immer和不使用immer的代码行数是一样的,也没体现出代码的简化,所以它的优点体现在哪里呢?

再看下面的例子:

// 不使用 immer,返回新状态,替换旧状态
reducers: {
  someReducer(state, action: PayloadAction<SourceData>) {
    return {
      ...state,
      first: {
        ...state.first,
        second: {
          ...state.first.second,
          third: {
            ...state.first.second.third,
            value: action.someValue,
          },
        },
      },
    };
  },
},

// 使用 immer,无需返回新状态,直接修改原状态
reducers: {
  someReducer(state, action: PayloadAction<SourceData>) {
    state.first.second.third.value = action.someValue;
  },
},

这是一个很典型的例子,如果我们的状态嵌套了很多层,并且需要修改的数据在很深层,这时immer的便利性就体现出来了,主要有以下两个好处:

五、核心api

这是对标准ReduxcreateStore函数的封装,它为store添加了一些配置,以获得更好的开发体验,包裹createStore,并集成了redux-thunkRedux DevTools,默认开启

返回一个包含默认middleware的数组,默认情况下,configureStore会自动添加一些中间件到store设置中。如果你想自定义middleware列表,你可以将自己的middleware添加到getDefaultMiddleware返回的数组中。

简化了标准Redux中的reducer函数。内置了immer(默认开启),通过在reducer中编写mutable代码,极大简化了immutable的更新逻辑(上文中有详细介绍过immer库),除此之外,在RTK中使用createReducer函数创建reducer的时候,有两中创建方式,一种回调函数的方式,一种映射对象的方式。(我更喜欢后者,因为映射对象的书写方式看起来更加直观、更加容易理解)

用于创造和定义标准Redux类型的函数。传入一个常量类型,它会返回一个携带payload的函数,和标准redux中的action基本类似。

这个createSlice函数,在我看来是RTK中的核心api,官方文档中对它的描述是这样的:该函数接收一个初始化state对象,和一个reducer对象,它可以将store以slice的方式分割成为不同的部分,每个部分都会独立生成相对应的actionstate对象。在99%的情况下,我们都不会直接使用createReducercreateAction,取而代之的就是createSlice

用来处理异步操作的方法,对于使用RTK的项目来说,完成异步操作主要分三个步骤,createAsyncThunk方法主要用来创建异步函数,创建完毕之后在reduce中进行处理,最后在业务代码中用dispatch进行调用,基本流程和标准的Redux并无二致。(需要注意的是,在createSlice中,我们不可以用普通的reduce处理异步函数,必须使用 extraReducers来处理异步)

六、如何使用

1. 安装

# 使用 npm
npm install @reduxjs/toolkit

# 使用 yarn
yarn add @reduxjs/toolkit

2. 初始化state

interface InitialState {
  count: number;
}

const initialState: InitialState = {
  count: 0,
};

3. 创建slice

export const getData = createSlice({
  name: "nameSpace",
  initialState,
  reducers: {},
  extraReducers: {},
});

4. 创建异步函数

const fetchData = createAsyncThunk("nameSpace/fetchData", async () => await axios(someAPI));

5. 传入异步函数,更新状态

const getData = createSlice({
  name: "nameSpace",
  initialState,
  reducers: {},
  extraReducers: {
    [fetchData.fulfilled.type]: (state: InitialState, action: PayloadAction<InitialState>) => {
      state.count = action.payload.count;
    },
  },
});

6. 创建Reducer

const rootReducer = combineReducers({ data: getData.reducer });

7. 创建仓库

const store = configureStore({
  reducer: rootReducer,
  middleware: getDefaultMiddleware => [...getDefaultMiddleware()],
  devTools: true,
});

export default store;

完整代码如下:

import { createSlice, PayloadAction, createAsyncThunk, combineReducers, configureStore } from "@reduxjs/toolkit";
import axios from "axios";

interface InitialState {
  count: number;
}

const initialState: InitialState = {
  count: 0,
};

export const fetchData = createAsyncThunk("nameSpace/fetchData", async () => await axios(someAPI));

export const getData = createSlice({
  name: "nameSpace",
  initialState,
  reducers: {},
  extraReducers: {
    [fetchData.fulfilled.type]: (state: InitialState, action: PayloadAction<InitialState>) => {
      state.count = action.payload.count;
    },
  },
});

const rootReducer = combineReducers({ data: getData.reducer });

const store = configureStore({
  reducer: rootReducer,
  middleware: getDefaultMiddleware => [...getDefaultMiddleware()], 
  devTools: true,
});

export default store;

8. 在业务代码中使用:

import React, { useEffect } from "react";
import { useDispatch } from "react-redux";
import { fetchData } from "../redux/slice";
import { useSelector } from "../redux/hooks";

const App: React.FC = () => {
  const dispatch = useDispatch();
  const count = useSelector(({ data }) => data);
  useEffect(() => {
    dispatch(fetchData());
  }, []);
  return <div>{count}</div>;
};

export default App;

七、最后

写的比较乱,当做学习笔记写的,后面有时间会持续进行优化和补充,如果有错误,感谢指正!