使用ReduxToolKit

Redux可能不是一种易于理解的状态数据管理模式,虽然备受一部分人推崇,但在早期的React版本中直接使用react-redux就是一场灾难,它让无数跟风入坑的前端开发者痛苦不堪。在新版本的React中,日常开发已经大多转向函数式组件,使用Redux的正确方式是使用Redux Toolkit(后续简称RTK)框架。

RTK是基于react-redux的又一层封装,它用起来仍然充斥着不直观的逻辑和大段模板式的代码,编写这些模板代码在对Redux不熟悉时出错的概率仍然非常高,此外也并未解决使用Redux对代码调试带来的负面影响,总而言之它现在仍然非常难用,但相比早期版本已经算得上是好用一些了。我们引入RTK时一定要仔细权衡,确定我们需要它时再引入。

官方文档:https://redux-toolkit.js.org/

安装依赖

我们需要在React工程中安装以下依赖。

npm i @reduxjs/toolkit react-redux

安装调试插件

除了安装框架本体,我们还需要安装redux-devtools,它用于调试Redux相关的数据。

npm install -D redux-devtools

安装后,还需要在Chrome或Firefox浏览器中安装调试插件。

安装好后,我们就可以在浏览器的调试工具中可视化的查看Redux相关的数据了。

RTK的基本使用

这里我们以一个例子的形式来直观感受RTK的用法,我们的工程目录结构如下。

src
  |_ store
    |_ features
      |_ counterSlice.js
      |_ ...
    |_ index.js
  |_ App.jsx
  |_ main.jsx

其中,main.jsx是React根组件,App.jsx是我们的一个自定义React组件。我们主要关注store目录,其中的index.js是RTK Store的入口,features中定义了一些Slice,counterSlice.js就是其中之一。

Slice:RTK中Slice可以理解为Store的一个片段,我们通常把一组相关的数据封装到一个Slice中。

例子代码如下,下面代码中实现了一个可以加减的计数器组件。

store/features/counterSlice.js

import { createSlice } from "@reduxjs/toolkit";

// 创建slice
const counterSlice = createSlice({
  name: "counter",
  initialState: {
    value: 0,
  },
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
  },
});
// 导出增加和减少的action函数
export const { increment, decrement } = counterSlice.actions;
// 导出reducer
export default counterSlice.reducer;

Slice中,我们使用了createSlice()函数,它是一个用来创建Slice的工具函数,传入其中的对象内容包括数据的初始值和相关的Reducer的逻辑。最后,我们导出了创建好的Action和Reducer。

store/features/index.js

import { configureStore } from "@reduxjs/toolkit";
import counterSliceReducer from "@/store/features/counterSlice";

// 创建store
const store = configureStore({
  reducer: {
    counter: counterSliceReducer,
  },
});
// 导出store
export default store;

index.js是RTK Store的入口,它将counterSlice相关的内容配置到Store中,并导出了store对象。

main.jsx

import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import store from "@/store";
import App from "./App.jsx";

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

main.jsx中,我们使用了RTK的<Provider>组件,并传入了Store对象,这样我们就可以在所有子组件中从Store读取数据了。

App.jsx

import { useDispatch, useSelector } from "react-redux";
import { increment, decrement } from "@/store/features/counterSlice";

const App = () => {
  // 获取store中名为counter的slice中的数据
  const value = useSelector((state) => state.counter.value);
  // dispatch用于分发action
  const dispatch = useDispatch();

  return (
    <div>
      <div>{value}</div>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(decrement())}>-</button>
    </div>
  );
};

export default App;

App.jsx中,我们通过useSelector()获取名字叫counter的这个Slice的数据,并将其显示在页面上,另外我们还设置了两个按钮,用于分派incrementdecrement这两个Action,分派后Store内的数据就会在Reducer的作用下改变,同时通过value再更新到页面上。

此外,Action中也可以传递参数,我们继续修改上面代码,新增一个点击一次+2的按钮。

<button onClick={() => dispatch(increment({step: 2}))}>+2</button>

代码中,我们在increment()内传递了一个对象{step: 2},在Redux中这个附加给Action的对象叫payload

increment: (state, { payload }) => {
  if (payload?.step) {
    state.value += payload.step;
  } else {
    state.value += 1;
  }
}

counterSlice.jsincrement定义中,{ payload }所在的参数对象就是被分发的Action,我们从其中读取了payload属性,并取出了它的step属性。

RTK的异步处理

看明白上面的代码后我们学习Redux就成功一半了,前面我们分发Action到Reducer处理数据是一个同步的操作,但实际开发中其实异步逻辑也有很多,例如使用FetchAPI调用接口,这里我们继续学习RTK中如何处理异步逻辑。

store/features/userSlice.js

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import { fetchUserList } from "@/services";

export const getUserList = createAsyncThunk(
  "user/fetchUserList",
  fetchUserList
);

const userSlice = createSlice({
  name: "user",
  initialState: {
    loading: false,
    total: 0,
    list: [],
  },
  extraReducers(builder) {
    builder
      .addCase(getUserList.pending, (state) => {
        state.loading = true;
      })
      .addCase(getUserList.fulfilled, (state, { payload }) => {
        state.loading = false;
        state.total = payload.length;
        state.list = payload;
      })
      .addCase(getUserList.rejected, (state) => {
        state.loading = false;
      });
  },
});

export default userSlice.reducer;

上面代码定义了一个名为user的Slice,这里我们用到了createAsyncThunk()函数,它是一个用于创建异步Action的辅助函数,它的第一个参数是Action的名字,第二个参数是具体的执行逻辑。

createSlice()函数中,我们在参数对象中定义了extraReducers(builder) { }方法,它用于将createAsyncThunk()定义的Action整合到Slice内,这里这个异步的Action对应3种状态会分别触发三种Reducer。

App.jsx

import { useDispatch, useSelector } from "react-redux";
import { getUserList } from "@/store/features/userSlice";

const App = () => {
  const { loading, total, list } = useSelector((state) => state.user);
  const dispatch = useDispatch();

  return (
    <div>
      {loading ? (
        <div>Loading...</div>
      ) : (
        <div>
          <div>total: {total}</div>
          <div>{JSON.stringify(list)}</div>
        </div>
      )}

      <button onClick={() => dispatch(getUserList())}>+</button>
    </div>
  );
};

export default App;

在App.jsx中,读取Slice中数据的写法和之前计数器例子完全相同,唯一的不同点是这里我们点击按钮时,分发的是一个异步Action。

此时我们点击按钮,就可以看到页面上先显示“Loading...”,异步请求完成后显示对应的数据。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。