状态管理方案的首选: Redux | 阅读文档后理解 (1)

59 阅读8分钟

Redux

quick learning

🍐 在使用 Redux 前,你如果使用过 React 原生的 useContext 与 useReducer 打配合管理 state,那么你会更容易理解,Redux 的 reducer action dispatch 等概念。

什么是 Redux

Redux 是一个使用叫做 “action” 的事件来管理和更新应用状态的模式和工具库 它以集中式 Store(centralized store)的方式对整个应用中使用的状态进行集中管理,其规则确保状态只能以可预测的方式更新。

useContext 与 Redux 都是 React 的状态管理解决方案,两者有何区别?

  1. 使用方式

    useContext 是 React 内置的 API,使用起来相对简单。它允许您创建上下文对象,并使用 useContext 钩子来访问这些对象,以在组件之间共享数据。这对于在少数几个组件之间共享少量数据非常有用。

    Redux 是一个独立的库,需要您设置 store、定义 actions 和编写 reducers 来管理应用程序的状态。这种方法可能比 useContext 更复杂,但它提供了更强大的状态管理功能。

  2. 状态管理

    useContext 主要是用于组件之间共享状态,但它不提供任何用于管理状态的内置工具。这意味着您需要手动管理状态,随着应用程序的增长,这可能会变得繁琐。

    Redux 提供了一个强大的状态管理系统,允许您以可预测的方式管理应用程序的状态。它为您的状态提供了一个单一的真相来源,并允许您分派 actions 更新状态。

  3. 性能

    useContext 通常很快且效率高,但如果有大量需要访问共享状态的组件,性能可能会下降。在这种情况下,您可能需要考虑使用不同的状态管理解决方案。

    Redux 被设计为在大量状态下也能保持高性能。它使用单向数据流和不可变数据结构,以确保状态更新高效且可预测。

  4. 学习曲线

    使用 useContext 相对容易学习,新手也可以使用它。它不需要任何额外的库或工具,所以很容易开始使用。

    Redux 的学习曲线可能更陡峭,特别是如果您是 React 的新手。它需要您学习新的概念,如 actions、reducers 和 store,可能需要一些时间才能掌握。

总的来说,useContext 是在组件之间共享状态的轻量级解决方案,而 Redux 提供了更强大的状态管理系统,更适合大型应用程序。选择两者之间的方案取决于您的应用程序复杂度和特定需求。

Redux Toolkit

Redux Toolkit 是一个官方推荐的 Redux 工具集,它的目的是简化 Redux 的使用和开发流程。

一般而言,使用 Redux 都是要用 Redux Toolkit 打配合的。所以我的学习就是直接跳过 Redux 原生的写法。

Redux Toolkit 有哪些好处

  1. 简化 Redux 的使用

    Redux Toolkit 提供了一些简化 Redux 的 API,例如 createSlice 和 configureStore,这些 API 可以大大减少编写 Redux 代码的数量,使代码更加简洁易读。

  2. 自动化 Redux 的最佳实践

    Redux Toolkit 遵循 Redux 的最佳实践,例如使用 Immer 库来处理不可变状态,并自动处理 Redux 中的常见问题,例如异步 Action 和代码拆分等。

  3. 提高开发效率

    使用 Redux Toolkit 可以显著提高开发效率,因为它可以减少您需要编写的代码量,从而减少出错机会。此外,Redux Toolkit 还提供了一些开发者工具,例如 Redux DevTools Extension,以便进行调试和分析。

  4. 更好的性能

    Redux Toolkit 与 Redux 配合使用时,可以使用 Redux 的性能优化技术,例如 memoization 和 selector,以提高应用程序的性能。

文件与关键词的关系

app/store.js | store


放置 Redux Store ,所有的状态都集中管理在这里。一般而言一个应用程序只有一个 store ,允许存在多个但不推荐。

类似于 context API,我们只是创建了一个 store 还需要将它提供给应用程序

在根组件外包裹 <Prodiver store={store}>

reducer 中的每个键值对就是一个切片,键名为 state 的名称,值为这个 state 的 reducer 逻辑

import { configureStore } from "@reduxjs/toolkit";
import counterReducer from '../features/counter/counterSlice';

export const store = configureStore({
    reducer: {
        counter: counterReducer
    }
})

features/counter/counterSlice.js | slice


Redux 的想法是将所有 state 相关的逻辑放在一起不太理想,故下分一个对象 slice ,切片。切片也就是与 state 相关的 reducer action 的集合。

slice 的 reducers 中的每个键值对就是一个 action,键名为 action.type,值为处理逻辑,Redux 中又称 reducer

reducer 接受的第一个参数是该切片的 state,第二个参数是 action 描述对象

导出的 slice.actions 其实是 actionCreator,函数,帮助我们生成 action(描述对象)

默认导出的 slice.reducer 用于配置 store,除此之外就不会有地方需要 slice.reducer 了

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

const initialState = {
    count: 0
}

const counterSlice = createSlice({
    name: 'counter',
    initialState,
    reducers: {
        increment: (state) => {
            state.count += 1;
        },
        decrement: (state) => {
            state.count -= 1;
        },
        reset: (state) => {
            state.count = 0;
        },
        incrementByAmount: (state, action) => {
            state.count += action.payload;
        }
    }
})

export const { increment, decrement, reset, incrementByAmount } = counterSlice.actions;

export default counterSlice.reducer;
✨ state 的更新理应采用不可变原则,即复制原来的 state ,更新后返回作为新的 state,但是 Redux Toolkit 在 slice 的 reducers 里使用了 Immer 库,我们修改的其实是 state 的 draft,表现得就好像是直接修改 state 一般,这样很方便。

请记住你只可以在 createSlice 当中像正常修改对象一样修改 state

features/counter/Counter.jsx | component


组件中想要使用集中管理的 state,只需要使用 useSelector,调用时描述到底想要仓库中的什么值

如果想要更新,使用 useDispatch 创建一个发布器,调用它传入 action 对象,即描述进行什么操作的对象,而之前在 slice 文件中有导出 action creator,调用它们能快捷生成对应的 action 描述对象

action creator 接受的参数会作为生成 action 描述对象的 payload 的值

import { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { decrement,increment, reset, incrementByAmount } from "./counterSlice";

const Counter = () => {
    const [incrementAmount, setIncrementAmount] = useState(0);
    const count = useSelector(state => state.counter.count);
    const dispatch = useDispatch();

    return (
        <div className="counter">
            <p>{count}</p>
            <div>
                <button onClick={() => dispatch(increment())}>+</button>
                <button onClick={() => dispatch(decrement())}>-</button>
            </div>

            <label htmlFor="incrementAmount">Amount: </label>
            <input id="incrementAmount" type="text" value={incrementAmount} onChange={e => setIncrementAmount(Number(e.target.value))} />

            <button onClick={() => dispatch(incrementByAmount(incrementAmount))}>Add</button> 
            <button onClick={() => dispatch(dispatch(reset()))}>Reset</button>
        </div>
    )
}

export default Counter

教程

🍐 接下来的教程是对官方文档层层递进的教学示例的理解说明,建议你在跟着官方一起敲完一节的代码后来看看我是怎么想的,补充一下你的理解,也当作一次及时的复习。Redux 的教学很长,如果仅是横冲向前不作歇息复习,到后面节数的时候很容易迷惑。

Redux 基础教程,第三节:数据流基础 | Redux 中文官网

数据流

选择器

useSelector 传入回调(选择器)接受参数为顶级的 state,所以选择器的职责就是描述想要拿到什么值。

对于多处使用仓库中的同一值,每次都现写一个选择器显得过于冗余,记得函数式编程大忌 DRY (Don’t repeat yourself),我们可以在这个值对应 slice 文件中提前写好选择器,并导出,需要用到的地方导入选择器并且使用即可。

为什么 Redux 要在 slice 文件中提前写好选择器导出使用,而不是直接导出想要得到的值呢?

虽然直接将状态的值导出并在组件中使用可能是可行的,但这样做将会破坏 Redux 的核心原则之一:单向数据流。在 Redux 中,组件不应该直接修改 store 中的状态,而应该通过 dispatching actions 来修改状态。如果在组件中直接使用 store 中的状态值,那么组件就会直接操作状态,这将违反 Redux 的设计理念。

另外,如果您在多个组件中多次使用相同的状态,那么将状态值直接导出并在组件中使用可能会导致代码冗余和不一致性。如果您需要修改状态,那么您需要在多个组件中查找并修改状态值,这将使代码更难以维护和调试。

相反,将状态值存储在 Redux store 中,并使用选择器从 store 中提取它们,不仅可以遵循 Redux 的设计原则,还可以使您的代码更加模块化和可重用。选择器提供了一个抽象层,使您可以将状态的复杂性隐藏在内部,并提供一个简单的接口来访问所需的状态。

综上所述,将状态的值直接导出并在组件中使用可能会导致代码冗余和不一致性,同时也会违反 Redux 的设计原则。因此,将状态存储在 Redux store 中,并使用选择器从 store 中提取它们,是更好的做法。

slice

function createSlice({
    // A name, used in action types
    name: string,
    // The initial state for the reducer
    initialState: any,
    // An object of "case reducers". Key names will be used to generate actions.
    reducers: Object<string, ReducerFunction | ReducerAndPrepareObject>
    // A "builder callback" function used to add more reducers, or
    // an additional object of "case reducers", where the keys should be other
    // action types
    extraReducers?:
    | Object<string, ReducerFunction>
    | ((builder: ActionReducerMapBuilder<State>) => void)
})

reducers 对象的属性可以是一个 ReducerFunction 也可以是一个 属性为 ReducerFunction 和 prepareFunction 的对象。

一般情况下,都是 ReducerFunction,但是如果更新 state 想要的数据包含一些自动生成值的话,比如 id date,就可以用 prepareFunction 准备这些值再返回作为 action.payload

const postsSlice = createSlice({
    name: 'posts',
    initialState,
    reducers: {
        postAdded: {
            reducer(state, action) {
                state.push(action.payload);
            },
            **prepare(title, content, userId) {
                return {
                    payload: {
                        id: nanoid(),
                        title,
                        content,
                        userId,
                        date: new Date().toISOString()
                    }
                }
            }**
        }
    }
}) 

异步逻辑

当调用 fetchTodosAsync action creator 时,它将返回一个 Promise。在 Promise 成功解析后,Redux store 中的状态将自动更新,从而触发相应的重新渲染。

createAsyncThunk 还提供了一些额外的功能,例如自动生成包含 pending、fulfilled 和 rejected action 的 action types,并自动处理异步操作期间的错误和加载状态。它还提供了一些选项,例如定义 action creator 的 payloadCreator 函数和设置 extraReducers,以进一步处理 action 的其他方面。