发布于 2026-01-06 2 阅读
0

React DEV 全球展示挑战赛:React Redux 实用入门,由 Mux 呈现:展示你的项目!

React 中 Redux 的实用入门

由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!

目录

  1. 介绍
  2. Redux是什么?
  3. 国家是什么?
  4. 如何修改状态?
  5. 单向数据流
  6. 在 React 应用中配置 Redux
  7. 使用 React Hooks 读取状态
  8. 使用 React Hooks 分发 Action
  9. 使用“json-server”进行本地伪造 API
  10. 异步操作
  11. 多级减速器
  12. 特色文件夹和鸭子
  13. 在我们的示例应用程序中使用“鸭子”图案
  14. 使用 Redux Toolkit(推荐)

介绍

在本教程中,我想简要解释一下什么是 Redux 以及如何在 React 项目中设置它。

如果您已经学习过 React,并且想了解 Redux 如何帮助管理应用程序的全局状态,那么本教程对您很有用。

我们很多人都听说过,使用 Redux 原生 API 编写的 Redux 代码比较冗长(例如,初始化 store、创建 actions 和 reducers 等)。因此,Redux 团队创建了一个名为Redux Toolkit 的工具包,旨在让 Redux 应用的开发更加轻松有趣。此外,使用Redux Toolkit编写 Redux 逻辑也是官方推荐的做法。

在本教程中,我们将从基础知识入手,然后仅使用 Redux(不使用工具包)构建一个简单的应用程序。最后,我们将添加 Redux Toolkit 来改进我们的 Redux 代码。

那么,我们开始吧。

Redux是什么?

Redux 是一个状态管理库。它通常与 React 一起使用,但也可以与其他视图库一起使用。Redux
帮助我们将整个应用程序的状态集中在一个地方进行管理。

国家是什么?

我会将“状态”定义为用于在任何给定时间渲染应用程序的数据。我们将这些数据保存在一个 JavaScript 对象中。例如,在一个渲染松饼列表的简单应用程序中,状态可能如下所示:

let state = {
  muffins: [
    { name: 'Chocolate chip muffin' },
    { name: 'Blueberry muffin' }
  ]
}
Enter fullscreen mode Exit fullscreen mode

如何修改状态?

要从组件内部修改状态,我们需要分发一个 action:

// SomeComponent.js
dispatch({
  type: 'muffins/add',
  payload: {
    muffin: { name: 'Banana muffin' },
  },
});
Enter fullscreen mode Exit fullscreen mode

派发操作是改变状态的唯一方法。

一个动作由一个带有属性的对象表示type。该type属性是动作的名称。你可以向该对象添加任何其他属性(这就是将数据传递给 reducer 的方式)。

如何命名你的操作没有正式规则。请给你的操作起描述性且有意义的名字。不要使用含义模糊的名字,例如 `a`receive_data或 `b` set_value

通过 action 创建函数来共享 action 是一种常见的做法。这些函数会创建并返回 action 对象。我们将 action 创建函数存储在组件文件之外(例如,src/redux/actions.js)。这样可以方便地查看应用程序中可用的 action,并便于维护和重用它们。

// actions.js
export function addMuffin(muffin) {
  return {
    type: 'muffins/add',
    payload: { muffin },
  };
}

// SomeComponent.js
dispatch(addMuffin({ name: 'Banana muffin' }));
Enter fullscreen mode Exit fullscreen mode

一旦一个 action 被分发,Redux 就会调用 reducer,并将之前的状态和已分发的 action 对象作为参数传递。Reducer 是一个函数,它决定如何根据给定的 action 来改变状态。我们需要创建这个函数并将其注册到 Redux 中。

这是一个基本的 reducer 结构:

let initialState = {
  muffins: [
    { id: 1, name: 'Chocolate chip muffin' },
    { id: 2, name: 'Blueberry muffin' },
  ],
};

function reducer(state = initialState, action) {
  switch (action.type) {
    case 'muffins/add':
      let { muffin } = action.payload;
      return { ...state, muffins: [...state.muffins, muffin] };
    default:
      return state;
  }
}
Enter fullscreen mode Exit fullscreen mode

当此 reducer 识别出muffins/add操作时,它会将给定的松饼添加到列表中。

重要提示: reducer 会复制之前的状态对象,而不是直接修改它。规则是状态必须是不可变的(只读)。reducer 在修改任何对象之前都应该先将其复制。这包括根对象和任何嵌套对象。

我们需要复制状态,以便 Redux 能够(使用浅相等性检查)判断 reducer 返回的状态是否与之前的状态不同。有关浅相等性检查的更多详细信息,请参阅:浅相等性检查和深相等性检查有何区别?。遵循此规则对于 Redux 正确响应状态变化至关重要。此外,当将 Redux 与 react-redux 一起使用时,这有助于 react-redux 决定在状态变化时应该重新渲染哪些组件。

另一条重要的规则是,reducer 函数必须是纯函数。给定相同的输入,它应该始终产生相同的输出,且不会产生任何副作用。副作用是指读取或改变函数周围环境的操作。例如,读取或写入全局变量、执行网络请求等都属于副作用。这条规则有助于我们根据特定的状态对象复现应用程序的外观和行为。

此外,这两条规则确保了 Redux 的时间旅行功能能够与我们的应用正常工作。时间旅行功能允许我们轻松撤销操作,然后再重新执行。这对于使用Redux DevTools进行调试非常有帮助。

总结起来:

  • 我们的应用程序只有一个状态。
  • 为了改变这种状态,我们会分发动作。
  • reducer 函数处理已分发的操作,并相应地更改状态。
  • Redux 和 react-redux 使用浅检查来检查 reducer 返回的状态是否发生变化

单向数据流

所以,我们了解了 Redux 的以下工作原理:我们从视图层(例如 React 组件)分发一个 action,reducer 接收到这个 action 并相应地修改 state,store 通知视图层 state 的变化,视图层根据最新的 state 渲染应用。当我们需要再次修改 state 时,这个循环就会重复进行。

因此,Redux 应用中的数据以单向循环模式流动,也称为单向数据流。我们可以用下图来表示它:

redux-data-flow

这种模式使理解 Redux 应用的工作原理变得更容易。

在 React 应用中配置 Redux

在这篇文章中,我们将构建一个简单的应用程序,用于列出松饼的数量。

我使用create-react-app创建了一个基本的 React 应用

npx create-react-app my-react-redux
Enter fullscreen mode Exit fullscreen mode

我移除了多余的代码,并渲染了一个硬编码的松饼列表。这就是我得到的结果:在 GitHub 上查看

我们先把松饼储存起来吧。

首先,我们来安装“redux”和“react-redux”软件包:

npm i -S redux react-redux
Enter fullscreen mode Exit fullscreen mode

请记住,Redux 可以与其他视图库一起使用。因此,我们需要“react-redux”包来将 React 组件与 Redux store 连接起来。

接下来,我们需要准备 Redux store。store 是一个对象,它保存应用程序的状态并提供用于操作状态的 API。它允许我们:

  • 阅读州
  • 调度操作以改变状态
  • 以及订阅/取消订阅状态变更

重要提示:您的应用应该只有一个应用商店。

接下来,我们来为示例应用搭建应用商店。

让我们把 Redux 功能放在名为“redux”的文件夹中:

mkdir src/redux
Enter fullscreen mode Exit fullscreen mode

让我们在文件src/redux/store.js中编写 store 初始化代码:

// File: src/redux/store.js
import { createStore } from 'redux';

const initialState = {
  muffins: [
    { id: 1, name: 'Chocolate chip muffin' },
    { id: 2, name: 'Blueberry muffin' },
  ],
};

const reducer = (state = initialState, action) => {
  switch (action.type) {
    default:
      return state;
  }
};

const store = createStore(reducer);

export default store;
Enter fullscreen mode Exit fullscreen mode

我们使用包createStore中的函数redux来创建 store。当 store 初始化时,它会通过调用我们的 reducer 函数,并undefined传入状态和一个虚拟操作(例如,reducer(undefined, { type: 'DUMMY' }))来获取初始状态。

现在我们需要将 store 提供给 React 组件。
为此,我们打开src/index.js文件,并将组件包装在来自“react-redux”包的组件<App />中:<Provider />

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import './index.css';
import App from './components/App';
import store from './redux/store';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
Enter fullscreen mode Exit fullscreen mode

<Provider />组件使用 React 上下文为子组件树提供 store。现在我们可以使用 React Hooks 或connect来自“react-redux”包的函数来获取状态,并从树中的任何组件分发 action。

在 GitHub 上查看代码

使用 React Hooks 读取状态

与其在“Muffins.js”中硬编码松饼列表,不如使用useSelector“react-redux”的钩子从状态中选择松饼数组。

// file: src/redux/selectors.js
export const selectMuffinsArray = (state) => state.muffins;
Enter fullscreen mode Exit fullscreen mode
// file: src/components/Muffins/Muffins.js
import React from 'react';
import { useSelector } from 'react-redux';
import { selectMuffinsArray } from '../../redux/selectors';

const Muffins = () => {
  const muffins = useSelector(selectMuffinsArray);

  return (
    <ul>
      {muffins.map((muffin) => {
        return <li key={muffin.id}>{muffin.name}</li>;
      })}
    </ul>
  );
};

export default Muffins;
Enter fullscreen mode Exit fullscreen mode

useSelector钩子函数期望第一个参数是一个选择器函数。我们创建选择器函数是为了提供一个可重用的 API,用于选择状态的不同部分。

我们在很多组件中都会用到状态。如果我们直接从状态中选择元素(例如 `{{ } let muffins = state.muffins`),并且之后状态的结构发生了变化(例如 `{{ }`state.muffins变为state.muffins.items`{{ }`),那么我们就必须修改每个直接访问状态属性的组件。而使用选择器函数,我们可以在同一个地方(在本例中是 `selectors.js` 文件)修改状态的选择方式。

在 GitHub 上查看代码

使用 React Hooks 分发 Action

让我们给列表中的每个松饼都添加一个“点赞”按钮。

首先,让我们向状态中添加“点赞数”属性。

// file: src/redux/store.js
const initialState = {
  muffins: [
    { id: 1, name: 'Chocolate chip muffin', likes: 11 },
    { id: 2, name: 'Blueberry muffin', likes: 10 },
  ],
};
Enter fullscreen mode Exit fullscreen mode

接下来,我们渲染点赞数和“点赞”按钮。

// file: src/components/Muffins/Muffins.js
<li key={muffin.id}>
  {muffin.name} <button>Like</button> <i>{muffin.likes}</i>
</li>
Enter fullscreen mode Exit fullscreen mode

现在,让我们使用“react-redux”中的 hookdispatch在组件中获取该函数。useDispatch

// file: src/components/Muffins/Muffins.js
import { useSelector, useDispatch } from 'react-redux';
// ...
const dispatch = useDispatch();
Enter fullscreen mode Exit fullscreen mode

让我们为“点赞”按钮定义一个操作。

// File: src/redux/actions.js
export const likeMuffin = (muffinId) => ({
  type: 'muffins/like',
  payload: { id: muffinId },
});
Enter fullscreen mode Exit fullscreen mode

接下来,我们来为“点赞”按钮创建“点击”事件处理程序:

// {"lines": "2,4-9,12"}
// file: src/components/Muffins/Muffins.js
import { likeMuffin } from '../../redux/actions';

// ...

{
  muffins.map((muffin) => {
    const handleLike = () => {
      dispatch(likeMuffin(muffin.id));
    };
    return (
      <li key={muffin.id}>
        {muffin.name} <button onClick={handleLike}>Like</button>{' '}
        <i>{muffin.likes}</i>
      </li>
    );
  });
}
Enter fullscreen mode Exit fullscreen mode

如果我们点击这个按钮,什么也不会发生,因为我们没有为要分发的操作创建 reducer(muffins/like)。

所以,我们来减少这项行动。

// {"lines": "4-14"}
// file: src/redux/store.js
const reducer = (state = initialState, action) => {
  switch (action.type) {
    case 'muffins/like':
      const { id } = action.payload;
      return {
        ...state,
        muffins: state.muffins.map((muffin) => {
          if (muffin.id === id) {
            return { ...muffin, likes: muffin.likes + 1 };
          }
          return muffin;
        }),
      };
    default:
      return state;
  }
};
Enter fullscreen mode Exit fullscreen mode

重要的是不要改变状态。因此,我复制状态对象,复制 muffins 数组(map 方法返回一个新数组)。最后,我只复制正在更改的 muffins。我没有修改其他 muffins,以表明它们没有改变。

现在,如果我们点击“点赞”按钮,muffins/like就会触发一个 action,reducer 会相应地改变状态。所选松饼的点赞数会增加。

在 GitHub 上查看代码

使用“json-server”进行本地伪造 API

“json-server”是一个模拟REST API的服务器,设置起来非常简单。我们可以在开发前端应用时用它来模拟API接口。本文的示例将使用这个服务器。接下来,我将向您展示如何安装和运行它。

安装步骤:

npm i -D json-server
Enter fullscreen mode Exit fullscreen mode

为了告诉服务器应该提供哪些数据,我们创建一个JSON文件。我们把它叫做db.json

{
  "muffins": [
    { "id": 1, "name": "Chocolate chip muffin", "likes": 11 },
    { "id": 2, "name": "Blueberry muffin", "likes": 10 }
  ]
}
Enter fullscreen mode Exit fullscreen mode

现在让我们打开package.json 文件,并添加启动此服务器的脚本:

// {"lines": "2"}
"scripts": {
  "json-server": "json-server --watch db.json --port 3001"
}
Enter fullscreen mode Exit fullscreen mode

运行方法:

npm run json-server
Enter fullscreen mode Exit fullscreen mode

服务器应该在http://localhost:3001上启动。

要停止它,请将焦点放在启动它的终端窗口中,然后按CTRL + C

我们可以使用以下路由(“json-server”通过查看db.json生成这些路由)。

GET /muffins
POST /muffins
PUT /muffins/{id}
DELETE /muffins/{id}
Enter fullscreen mode Exit fullscreen mode

在 GitHub 上查看代码

异步操作

请查看“使用‘json-server’进行本地模拟 API”部分

通常,我们会发起网络请求来获取和编辑数据。让我们看看如何使用 Redux 来实现这一点。

默认情况下,Redux 只允许我们以带有属性的对象的形式分发 action type

然而,Redux 允许我们使用中间件函数来改变它分发 action 的方式。其中一个这样的函数叫做“redux-thunk”。

让我们安装并向 Redux 注册这个中间件函数。

npm i -S redux-thunk
Enter fullscreen mode Exit fullscreen mode
// file: src/redux/store.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
// ...
const store = createStore(reducer, applyMiddleware(thunk));
Enter fullscreen mode Exit fullscreen mode

applyMiddleware是一个实用函数,它接受一个中间件函数列表,并将它们组合成一个中间件函数,我们将其createStore作为第二个参数传递给它。

另外,让我们在初始状态下清空 muffins 数组,因为我们将从 fake API 加载 muffins。

// file: src/redux/store.js
const initialState = {
  muffins: [],
};
Enter fullscreen mode Exit fullscreen mode

“redux-thunk”允许我们不仅分发对象,还分发函数:

dispatch((dispatch, getState) => {
  let state = getState();
  // do something async and
  dispatch(/* some action */);
});
Enter fullscreen mode Exit fullscreen mode

thunk 函数将原始dispatch函数作为第一个参数,将getState函数作为第二个参数。

因此,我们可以使用 thunk 函数来获取数据,例如,当数据准备就绪时,我们可以分发一个包含此数据的 action 对象,以便 reducer 可以将此数据添加到状态中。

让我们创建actions.js文件,并添加用于加载松饼的异步操作创建函数。

// file: src/redux/actions.js
export const loadMuffins = () => async (dispatch) => {
  dispatch({
    type: 'muffins/load_request',
  });

  try {
    const response = await fetch('http://localhost:3001/muffins');
    const data = await response.json();

    dispatch({
      type: 'muffins/load_success',
      payload: {
        muffins: data,
      },
    });
  } catch (e) {
    dispatch({
      type: 'muffins/load_failure',
      error: 'Failed to load muffins.',
    });
  }
};
Enter fullscreen mode Exit fullscreen mode

thunk 函数可以是同步的,也可以是异步的。我们可以在这个函数中分发多个 action。在我们的示例中,我们分发一个muffins/load_requestaction 来表示请求开始。我们可以使用此 action 在应用程序的某个位置显示一个加载指示器(spinner)。然后,当请求成功时,我们分发muffins/load_success包含已获取数据的 action。最后,如果请求失败,我们分发一个muffins/load_failureaction 来向用户显示错误消息。

现在,让我们为这些操作创建 reducer。

// file: src/redux/store.js
const reducer = (state = initialState, action) => {
  switch (action.type) {
    // ...
    case 'muffins/load_request':
      return { ...state, muffinsLoading: true };

    case 'muffins/load_success':
      const { muffins } = action.payload;
      return { ...state, muffinsLoading: false, muffins };

    case 'muffins/load_failure':
      const { error } = action;
      return { ...state, muffinsLoading: false, error };
    // ...
  }
};
Enter fullscreen mode Exit fullscreen mode

让我们loadMuffinsMuffins组件挂载时分发该操作。

// file: src/components/Muffins/Muffins.js
import React, { useEffect } from 'react';
import { loadMuffins } from '../../redux/actions';

// ...

const dispatch = useDispatch();

useEffect(() => {
  dispatch(loadMuffins());
}, []);
Enter fullscreen mode Exit fullscreen mode

我们在 effect hook 中加载 muffins,因为 dispatch 一个 action 会产生副作用。

最后,我们来处理加载和错误状态。

创建以下选择器函数:

// file: src/redux/selectors.js
export const selectMuffinsLoading = (state) => state.muffinsLoading;
export const selectMuffinsLoadError = (state) => state.error;
Enter fullscreen mode Exit fullscreen mode

并渲染加载和错误信息:

// file: src/components/Muffins/Muffins.js
const muffinsLoading = useSelector(selectMuffinsLoading);
const loadError = useSelector(selectMuffinsLoadError);

// ...

return muffinsLoading ? (
  <p>Loading...</p>
) : loadError ? (
  <p>{loadError}</p>
) : muffins.length ? (
  <ul>
    {muffins.map((muffin) => {
      // ...
    })}
  </ul>
) : (
  <p>Oh no! Muffins have finished!</p>
);
Enter fullscreen mode Exit fullscreen mode

现在,让我们检查一下我们是否都做对了。

我们应该运行本地的“json-server”和应用程序。

在一个终端窗口中:

npm run json-server
Enter fullscreen mode Exit fullscreen mode

而在另一个方面:

npm start
Enter fullscreen mode Exit fullscreen mode

在浏览器中,您应该会看到松饼列表,该列表现在是从虚假 API 服务器获取的。

在 GitHub 上查看代码

多级减速器

通常,在大型应用程序中,状态不会那么简单。它会看起来像一棵巨大的数据树。

reducer 函数会变得臃肿。

因此,将 reducer 拆分成多个较小的 reducer 是一个好主意,每个 reducer 只处理状态的一部分。

例如,为了处理上图中所示的状态,最好创建 3 个 reducer:

const muffinsReducer = (state = initialMuffinsState, action) => {
  // ...
};
const notificationsReducer = (state = initialNotificationsState, action) => {
  // ...
};
const cartReducer = (state = initialCartState, action) => {
  // ...
};
Enter fullscreen mode Exit fullscreen mode

并使用名为以下的实用函数将它们组合起来combineReducers

const rootReducer = combineReducers({
  muffins: muffinsReducer,
  notifications: notificationsReducer,
  cart: cartReducer,
});

const store = createStore(rootReducer);
Enter fullscreen mode Exit fullscreen mode

combineReducers创建一个根 reducer 函数,该函数在 action 被分发时调用每个子 reducer,并将它们返回的状态部分合并成一个单独的状态对象:

{
  muffins: ...,
  notifications: ...,
  cart: ...
}
Enter fullscreen mode Exit fullscreen mode

组合使用 reducer 可以方便地实现 reducer 逻辑的模块化。

特色文件夹和鸭子

Redux 文档建议将 Redux 功能组织成功能文件夹或 ducks

功能文件夹

与其按代码类型(例如,将所有应用程序的操作放在 actions.js 中,将所有 reducers 放在 reducers.js 中)对所有操作和 reducers 进行分组,不如按功能进行分组。

假设有两个功能:“用户”和“通知”。我们可以将它们的操作和 reducer 放在不同的文件夹中。例如:

redux/
  users/
    actions.js
    reducers.js
  notifications/
    actions.js
    reducers.js
  store.js
Enter fullscreen mode Exit fullscreen mode

鸭子

“鸭子模式”指的是我们应该将特定功能的所有 Redux 逻辑(action、reducer、selector)放在一个单独的文件中。例如:

redux/
  users.js
  notifications.js
  store.js
Enter fullscreen mode Exit fullscreen mode

在我们的示例应用程序中使用“鸭子”图案

在这个应用中,我们围绕着松饼(muffins)使用了不同的 Redux 功能。我们可以把这些功能归类到一个“鸭子”(duck,可能指某种功能或组件)中。换句话说,我们只需把所有与松饼相关的代码都移到一个 JavaScript 文件中,并将其命名为src/redux/muffins.js

让我们把操作、选择器和 reducer 移到这个文件中:

export const likeMuffin = (muffinId) => ({
  type: 'muffins/like',
  payload: { id: muffinId },
});

export const loadMuffins = () => async (dispatch) => {
  dispatch({
    type: 'muffins/load_request',
  });

  try {
    const response = await fetch('http://localhost:3001/muffins');
    const data = await response.json();

    dispatch({
      type: 'muffins/load_success',
      payload: {
        muffins: data,
      },
    });
  } catch (e) {
    dispatch({
      type: 'muffins/load_failure',
      error: 'Failed to load muffins.',
    });
  }
};

export const selectMuffinsArray = (state) => state.muffins;
export const selectMuffinsLoading = (state) => state.muffinsLoading;
export const selectMuffinsLoadError = (state) => state.error;

const initialState = {
  muffins: [],
};

const reducer = (state = initialState, action) => {
  switch (action.type) {
    case 'muffins/like':
      const { id } = action.payload;
      return {
        ...state,
        muffins: state.muffins.map((muffin) => {
          if (muffin.id === id) {
            return { ...muffin, likes: muffin.likes + 1 };
          }
          return muffin;
        }),
      };

    case 'muffins/load_request':
      return { ...state, muffinsLoading: true };

    case 'muffins/load_success':
      const { muffins } = action.payload;
      return { ...state, muffinsLoading: false, muffins };

    case 'muffins/load_failure':
      const { error } = action;
      return { ...state, muffinsLoading: false, error };

    default:
      return state;
  }
};

export default reducer;
Enter fullscreen mode Exit fullscreen mode

现在,在src/redux/store.js文件中,让我们使用以下函数创建根 reducer combineReducers

// {"lines": "6-10"}
// File: src/redux/store.js
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import muffinsReducer from './muffins';

const rootReducer = combineReducers({
  muffins: muffinsReducer,
});

const store = createStore(rootReducer, applyMiddleware(thunk));

export default store;
Enter fullscreen mode Exit fullscreen mode

现在,应用程序的状态如下所示:

{
  muffins: {
    muffins: [],
    muffinsLoading: boolean,
    error: string
  }
}
Enter fullscreen mode Exit fullscreen mode

由于状态结构发生了变化,为了使应用程序正常工作,我们需要更新读取状态的代码部分。幸运的是,我们使用选择器函数来选择状态对象的各个部分,而不是直接操作状态对象本身。因此,我们只需要更新选择器函数即可:

// File: src/redux/muffins.js
export const selectMuffinsState = (rootState) => rootState.muffins;

export const selectMuffinsArray = (rootState) =>
  selectMuffinsState(rootState).muffins;

export const selectMuffinsLoading = (rootState) =>
  selectMuffinsState(rootState).muffinsLoading;

export const selectMuffinsLoadError = (rootState) =>
  selectMuffinsState(rootState).error;
Enter fullscreen mode Exit fullscreen mode

最后,我们来更新导入语句:

// {"lines": "6,7"}
// File: src/components/Muffins/Muffins.js
import {
  selectMuffinsArray,
  selectMuffinsLoading,
  selectMuffinsLoadError,
} from '../../redux/muffins';
import { likeMuffin, loadMuffins } from '../../redux/muffins';
Enter fullscreen mode Exit fullscreen mode

就是这样!我们使用了“鸭子”模式,将管理 muffins 状态的 Redux 功能移到了一个单独的文件中。

在 GitHub 上查看代码

使用 Redux Toolkit(推荐)

Redux 团队建议使用Redux Toolkit来编写 Redux 逻辑。该工具包包含一系列实用工具,可以简化 Redux 应用的编写。纯 Redux 代码较为冗长,因此该工具包将原本需要编写的复杂代码封装成实用工具,从而帮助您减少代码量。此外,它还包含一些与 Redux 常用的其他库。

让我们使用 Redux Toolkit 来改进我们的 Redux 代码。

该工具包以独立软件包的形式分发。让我们来安装它:

npm i -S @reduxjs/toolkit
Enter fullscreen mode Exit fullscreen mode

然后,让我们打开src/redux/store.js并对其进行更新,以使用 Redux Toolkit 初始化 store。

// src/redux/store.js
import { configureStore } from '@reduxjs/toolkit';
import muffinsReducer from './muffins';

const store = configureStore({
  reducer: {
    muffins: muffinsReducer,
  },
});

export default store;
Enter fullscreen mode Exit fullscreen mode

我们用一个函数替换了createStoreapplyMiddlewarecombineReducers。该函数封装了 Redux ,添加了默认配置,并提供了配置 store 的附加功能。redux-thunkconfigureStorecreateStore

configureStore默认情况下会应用 Thunk 中间件,因此我们无需手动设置,也无需安装该redux-thunk包。此外,此函数会自动合并 reducer,因此我们不再需要 Redux combineReducers。我们将用于处理状态不同部分的 reducer 添加到reducer对象中。

如需了解更多信息,configureStore访问其文档

Redux Toolkit 包含强大的功能,可以帮助我们创建 reducer。其中有一个名为 `reducer.getReducer()` 的函数createReducer(initialState, caseReducers)。第一个参数是初始状态,第二个参数是一个对象,该对象将 action 类型映射到处理这些 action 的 reducer 函数。

接下来,我们将使用createReducer它来创建 reducer。请在src/redux/muffins.js 文件中,将旧的 reducer 代码替换为新的代码:

import { createReducer } from '@reduxjs/toolkit';

// ...

const reducer = createReducer(initialState, {
  'muffins/like': (state, action) => {
    const { id } = action.payload;

    return {
      ...state,
      muffins: state.muffins.map((muffin) => {
        if (muffin.id === id) {
          return { ...muffin, likes: muffin.likes + 1 };
        }
        return muffin;
      }),
    };
  },

  'muffins/load_request': (state) => {
    return { ...state, muffinsLoading: true };
  },

  'muffins/load_success': (state, action) => {
    const { muffins } = action.payload;
    return { ...state, muffinsLoading: false, muffins };
  },

  'muffins/load_failure': (state, action) => {
    const { error } = action;
    return { ...state, muffinsLoading: false, error };
  },
});
Enter fullscreen mode Exit fullscreen mode

这样看起来好多了,它更具声明性,每个操作都由自己的 reducer 函数处理,而switch之前的语句中作用域是在多个case操作之间共享的。

我们不应该就此止步,我们可以借助以下方法进一步改进这个归约器createReducer

我在之前的文章中提到过,当 reducer 函数改变状态时,不应该直接修改之前的状态。因此,在我们的 reducer 中,我们总是返回一个新的状态对象,并将需要修改的状态部分复制到新对象中,这样 Redux 就能创建新的引用,从而快速地将新旧状态进行比较,判断状态是否发生了变化。

在这个createReducer函数中,我们不再需要复制状态对象,可以直接修改它。该函数应用了Immer 库,将我们的修改操作转换为不可变的更新操作。让我们把原本难以阅读的不可变状态更新代码转换成易于阅读的可变版本,Immer 会在后台处理使其变为不可变:

const reducer = createReducer(initialState, {
  'muffins/like': (state, action) => {
    const muffinToLike = state.muffins.find(
      (muffin) => muffin.id === action.payload.id
    );
    muffinToLike.likes += 1;
  },

  'muffins/load_request': (state) => {
    state.muffinsLoading = true;
  },

  'muffins/load_success': (state, action) => {
    state.muffinsLoading = false;
    state.muffins = action.payload.muffins;
  },

  'muffins/load_failure': (state, action) => {
    state.muffinsLoading = false;
    state.error = action.error;
  },
});
Enter fullscreen mode Exit fullscreen mode

这段代码可读性好多了,对吧?不过,还是有一些需要注意的地方。在 reducer 中修改 state 时,务必做到要么修改 state 参数,要么返回一个新的 state。两者不可兼得。另外,请务必阅读Immer 的文档,了解使用 Immer 时可能遇到的问题

重要提示:createReducer你只能在 ` and`函数内部修改状态。稍后createSlice我会详细讲解。createSlice

请查阅createReducer文档(https://redux-toolkit.js.org/api/createReducer)以了解更多信息。

现在让我们看看我们可以用这些操作做什么。Redux Toolkit 提供了一个名为 `generated action creators` 的辅助函数createAction

让我们likeMuffin使用以下命令生成操作createAction

// src/redux/muffins.js
import { createReducer, createAction } from '@reduxjs/toolkit';

// export const likeMuffin = (muffinId) => ({
//   type: 'muffins/like',
//   payload: { id: muffinId },
// });
export const likeMuffin = createAction('muffins/like', (muffinId) => {
  return { payload: { id: muffinId } };
});
Enter fullscreen mode Exit fullscreen mode

createAction它接受两个参数。第一个参数是动作类型,它是必需的。第二个参数是所谓的准备函数,你可以使用它来接收来自生成动作的创建者的参数,并将这些参数作为附加数据附加到动作对象上。准备函数是可选的。

创建的动作createAction会重写其toString方法,使其返回动作类型。因此,如果我们把新的likeMuffin动作创建器放在 JavaScript 期望接收字符串的地方,likeMuffin它会通过该方法转换为“muffins/like”字符串likeMuffin.toString()。这意味着我们可以将新的动作创建器用作 reducer 中的动作类型键:

// src/redux/muffins.js
const reducer = createReducer(initialState, {
  // 'muffins/like': (state, action) => {
  [likeMuffin]: (state, action) => {
    // ...
  },
  // ...
});
Enter fullscreen mode Exit fullscreen mode

我们还有另一个操作loadMuffins——Thunk 操作。Redux Toolkit 提供了一个名为 `redo_thunk_action` 的辅助函数来生成 Thunk 操作createAsyncThunk。让我们使用这个函数来重写我们的loadMuffinsThunk 操作:

// src/redux/muffins.js
export const loadMuffins = createAsyncThunk('muffins/load', async () => {
  const response = await fetch('http://localhost:3001/muffins');
  const muffins = await response.json();
  return { muffins };
});
Enter fullscreen mode Exit fullscreen mode

createAsyncThunk该函数以操作类型作为第一个参数,以回调函数作为第二个参数。回调函数应返回一个 Promise 对象。Promise 的解析结果将被添加到操作对象的payload属性中。

createAsyncThunk返回一个 thunk action creator。当我们分发这个 action creator 时,它会根据回调返回的 promise 分发以下生命周期 action:pending( muffins/load/pending )、fulfilled( muffins/load/fulfilled ) 和rejected( muffins/load/rejected )。这些生命周期 action 的类型可以通过 action creator 的属性获取(例如,loadMuffins.pending)。

所以,让我们在 reducer 中使用这些类型muffins/load_request,而不是我们自己的类型muffins/load_successmuffins/load_failure

// src/redux/muffins.js
const reducer = createReducer(initialState, {
  // ...
  [loadMuffins.pending]: (state) => {
    state.muffinsLoading = true;
  },

  [loadMuffins.fulfilled]: (state, action) => {
    state.muffinsLoading = false;
    state.muffins = action.payload.muffins;
  },

  [loadMuffins.rejected]: (state) => {
    state.muffinsLoading = false;
    state.error = 'Failed to load muffins.';
  },
});
Enter fullscreen mode Exit fullscreen mode

最后,我们可以将与单个功能(例如松饼)相关的 Redux 功能分组到一个所谓的“切片”(或“鸭子”)中。为此,我们将使用以下createSlice函数。让我们打开src/redux/muffins.js并使用该函数重新组织我们的 Redux 逻辑createSlice

// src/redux/muffins.js
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';

// ...

// Selectors...

// ...

const muffinsSlice = createSlice({
  name: 'muffins',
  initialState,
  reducers: {
    likeMuffin: {
      reducer: (state, action) => {
        const muffinToLike = state.muffins.find(
          (muffin) => muffin.id === action.payload.id
        );
        muffinToLike.likes += 1;
      },
      prepare: (muffinId) => {
        return { payload: { id: muffinId } };
      },
    },
  },
  extraReducers: {
    [loadMuffins.pending]: (state) => {
      state.muffinsLoading = true;
    },

    [loadMuffins.fulfilled]: (state, action) => {
      state.muffinsLoading = false;
      state.muffins = action.payload.muffins;
    },

    [loadMuffins.rejected]: (state) => {
      state.muffinsLoading = false;
      state.error = 'Failed to load muffins.';
    },
  },
});

export const { likeMuffin } = muffinsSlice.actions;

export default muffinsSlice.reducer;
Enter fullscreen mode Exit fullscreen mode

乍一看,这个变化似乎有点令人困惑。所以,我们来逐条讨论一下。

首先,我们不再需要createReducercreateAction,因为createSlice为我们创建了 reducer 函数和基本(非 thunk)操作。

createSlice它需要一个切片的名称,我们可以用创建切片所针对的特征来命名,例如“松饼”createSlice 。该名称将用作通过该选项创建的操作类型的前缀reducers

然后,我们提供initialState切片。

接下来,createSlice提供了创建 reducer 的两种选择:reducersextraReducers

我们使用reducers`actions` 和对应的 reducer。`option`reducers是一个对象,它将 action 类型映射到对应的 reducer 函数。`actions`createSlice会接收这个映射,并从中生成 action 和 reducer。如果一个 action 除了 action 类型之外不需要保存任何数据,我们可以像这样创建 action 和 reducer:

createSlice({
  name: 'someSliceName',
  reducers: {
    helloWorld: (state) => {
      state.message = 'Hello World';
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

这将创建一个名为 `action creator` 的函数,helloWorld该函数返回以下操作对象:{ type: 'someSliceName/helloWorld' }。如果我们需要向操作对象添加其他数据,例如一些有效负载,我们可以添加以下prepare函数:

createSlice({
  name: 'someSliceName',
  reducers: {
    helloWorld: {
      reducer: (state, action) => {
        state.message = `Hello, ${action.payload.name}`;
      },
      prepare: (name) => {
        return { payload: { name } };
      },
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

此示例创建操作创建器helloWorld(name),该创建器接受name参数并返回以下操作对象:{ type: 'someSliceName/helloWorld', payload: { name } }

我们可以使用extraReducersreducer 来为现有的 action 和 thunk action 创建 reducer。基本上,你可以从另一个 slice 导入一个 action 并在这里进行处理。在我们的示例中,我们使用它extraReducers来处理loadMuffinsthunk action 的生命周期操作。

reducers两者的区别extraReducers在于,它createSlice不会自动为 reducer 生成 action creators extraReducers

两者reducersextraReducers允许我们修改状态参数,因为它们都会传递给createReducerImmer,Immer 会将我们的状态修改转换为不可变的更新。

createSlice返回一个具有以下结构的对象:

{
  name: name of the slice
  reducer: reducer function that combines reducers from `reducers` and `extraReducers` options
  actions: action creators extracted from the `reducers` option
  caseReducers: reducer functions from the `reducers` option
}
Enter fullscreen mode Exit fullscreen mode

在我们的示例中,我们从组件中提取出 action creatorsmuffinsSlice.actions并单独导出,以便于在其他组件中导入和使用它们。默认情况下,我们会导出 reducer 函数。

因此,借助 Redux Toolkit,我们的代码变得更短、更具声明性,从而更容易阅读和理解。

你已经完成了 Redux + React 的入门教程。我尽量让它简短易懂。我建议你查看Redux 文档和Redux Toolkit 网站上的Redux Essentials 系列教程。它们涵盖了许多细节、最佳实践以及本教程中未涉及的 Redux 和 Redux Toolkit 相关内容。

非常感谢您阅读我的教程。

请在 GitHub 上查看最终代码。

文章来源:https://dev.to/ddmytro/a-practical-introduction-to-using-redux-with-react-1a0m