React + Redux 和 Redux 工具包

🍕 什么是 Redux 以及状态的定义

Redux 是一个用于跨组件或应用程序范围状态的状态管理系统。我们可以将状态的定义分为三种主要类型:

Local State 🔸是属于单个组件的状态,例如切换状态。它使用useState()或useReducer()从组件内部进行管理。

跨组件状态🔸它是一种影响多个组件的状态,例如触发模态覆盖的按钮,可以从某处的按钮打开并由模态内的另一个按钮关闭。useState()或useReducer()可以通过使用props来管理此类行为。

App-wide state 🔸它是一种影响应用程序所有组件的状态,例如用户身份验证,登录后导航栏显示更多选项,其他组件更新。

🌮 Redux 与 React 上下文

React Context是一个集成功能,它还允许我们通过创建管理状态的上下文提供程序组件来避免 prop 链。但是,它有一些潜在的缺点,可能不会影响应用程序的开发。需要注意的是,应用程序不限于一种状态管理,两者都可以在同一个应用程序中使用。通常,只有一个用于应用程序的宽状态,React Context仍然可以用于选定的多组件状态,这在应用程序的某些部分很重要。

React 上下文缺点

  • 它在复杂的设置中效果不佳 🔹 这取决于应用程序的大小,但对于中小型应用程序可能不是问题。但是,在具有影响多个组件和许多上下文提供者的各种状态的大型应用程序中,开发人员最终可能会得到深度嵌套的 JSX 代码。

  • 性能🔹 据 React 团队的一位成员介绍,useContext的使用对于低频率更新(更改主题、身份验证等)非常有用,但在数据变化很大时效果不佳,因此不是替代类似通量的状态传播。另一方面,Redux是一个类似 Flux 的状态管理库,而useContext并不是它的好替代品。

(如果您想了解更多关于Flux 架构模式的信息,这里是一个链接)

🥞 理论上的 Redux

Redux的主要目标是创建一个中央数据存储,负责管理整个应用程序的状态。确实,在这个 store 中,可以处理所有的跨组件状态,如认证、主题化、用户输入等。 store 中包含的数据可以在组件中使用,如果某些数据发生变化,组件可以做出相应的反应并更新 UI。为此,组件订阅中央存储,当数据发生变化时,存储会通知组件。

组件如何更改存储中的数据

组件从不直接操作存储数据。基本上,组件通过订阅可以直接访问存储数据,但没有直接数据流向另一个方向。相反,需要设置的reducer函数负责改变存储数据(术语reducer已经表示一种接受一些输入、转换输入并返回新结果的函数)。

Actions用于触发reducer函数,而不是直接访问 store,组件可以调度这些actions。然后该操作被转发到reducer,它读取并执行所需操作的描述。reducer返回一个新状态,它替换中央数据存储中的现有状态。最后,当状态更新时,所有订阅组件都会收到通知,以便它们可以更新 UI。

🥐 安装 Redux

以下说明可以在Redux 官方网站上找到。

"推荐使用 React 和 Redux 启动新应用的方法是使用官方 Redux+JS 模板或 Redux+TS 模板来创建 React App,它利用了 Redux Toolkit 和 React Redux 与 React 组件的集成。 "

# Redux + Plain JS template
npx create-react-app my-app --template redux

# Redux + TypeScript template
npx create-react-app my-app --template redux-typescript

" Redux 核心库作为 NPM 上的一个包提供,可与模块捆绑器或 Node 应用程序一起使用: "

# NPM
npm install redux

# Yarn
yarn add redux

🌭 实战前的 Redux 基础

现在我们玩Redux来了解它的机制。

在应用程序中创建一个新的 JS 文件(我称之为 redux-demo.js)后,可以从Redux中导入createStore函数并使用它来创建我们的商店。

import {createStore} from 'redux';

const store = createStore();

createStore函数需要一个参数,该参数是reducer 用于生成新的状态快照。reducer还使用默认操作执行,该操作应返回初始状态

// Giving state a default value of {counter: 0}
const counterReducer = (state = {counter: 0}, action) => {
    return {
        counter: state.counter + 1
    }
};

const store = createStore(counterReducer);

创建reducer函数时,必须包含两个参数,旧状态和分派动作。此外,它必须始终返回一个新的状态对象。因此,reducer函数是一个“纯函数”,这意味着相同的输入值应该始终产生完全相同的输出,并且内部不应该有 HTTP 或本地存储请求等副作用。

const counterSubscriber = () => {
    const latestState = store.getState();
    console.log(latestState)
};

store.subscribe(counterSubscriber);

Redux通过订阅store并将订阅者函数本身作为参数传递来了解订阅者函数。

.getState()返回更新后的最新状态快照,因此它在状态更改后运行。

store.dispatch({type: "increment"});

分派是一种分派动作的方法。action是一个带有type属性的JS 对象,它充当标识符,通常是唯一的字符串。

尝试使用命令node [file name].js(在我的例子中 为 node redux-demo.js )在终端上运行以下代码:

import {createStore} from 'redux'

const counterReducer = (state = {counter: 0}, action) => {
    return {
        counter: state.counter + 1
    }
};

const store = createStore(counterReducer);

const counterSubscriber = () => {
    const latestState = store.getState();
    console.log(latestState)
};

store.subscribe(counterSubscriber);

store.dispatch({type: "increment"});

(如果您收到“ SyntaxError: Cannot use import statement outside a module ”,只需在 package.json 中添加 “type”:“module” )

在终端上,您现在应该能够看到:

{ counter: 2 }

计数器在初始化时增加,并在分派操作时再次增加。

尽管一切正常,但这并不是Redux所期望的行为,因为主要目标是在 reducer 中为不同的操作做不同的事情。因此,是时候使用 reducer 函数的第二个参数action了。

import {createStore} from 'redux'

const counterReducer = (state = {counter: 0}, action) => {
    if (action.type === "increment") {
        return {counter: state.counter + 1}
    }

    if (action.type === "decrement") {
        return {counter: state.counter - 1}
    }

    return state;
};

const store = createStore(counterReducer);

const counterSubscriber = () => {
    const latestState = store.getState();
    console.log(latestState)
};

store.subscribe(counterSubscriber);

store.dispatch({type: "increment"});
store.dispatch({type: "decrement"});

如果我们再次运行该文件,结果应该是:

{ counter: 1 }
{ counter: 0 }

初始化时,reducer返回原始状态,之后状态由调度函数递增和递减,计数器为 0。

🍝 React + Redux 实战

在现有应用程序中实现 Redux。

由于Redux不是特定于 react 的,但可以在任何 JavaScript 项目中使用,我们还将使用第二个名为react-redux的包。

这个包包括一个组件以使商店对整个应用程序可用,以及一对自定义钩子useSelectoruseDispatch允许组件与商店交互。

// In the terminal
npm i redux react-redux

更多信息可以在官方网站上找到。

按照惯例,通常会创建一个名为store的文件夹并将所有与Redux相关的文件放在其中。然后我创建一个index.js文件,但是你可以随意调用它。

让我们像之前一样创建一个简单的计数器,并导出store变量以提供应用程序的组件:

// src/store/index.js

import {createStore} from "redux"

function counterReducer(state = {counter: 0}, action) {
    switch (action.type) {
        case 'increment':
            return {counter: state.counter + 1}
        case 'decrement':
            return {counter: state.counter - 1}
        default:
            return state
    }
}

const store = createStore(counterReducer)

export default store

src/index.js文件中,它位于我们的应用程序树的顶部,也是我们渲染整个应用程序的地方,我们导入:

import {Provider} from "react-redux";

随着我们可以包装 , 并通过我们的store来存储Provider 的 store prop

// src/index.js

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

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Provider store={store}><App/></Provider>);

在 React 组件中使用 Redux 数据

要从组件中访问store数据,我们可以从 react redux 库中导入useSelectoruseStore钩子。第一个非常方便,因为它允许我们从商店中提取某些变量。但是,react-redux 文档建议您通常更喜欢useSelector

useSelector:当一个动作被调度时, useSelector() 将对先前的选择器结果值和当前结果值进行参考比较。如果它们不同,组件将被强制重新渲染。如果它们相同,则组件不会重新渲染。

useStore:这个钩子可能不应该经常使用。首选 useSelector() 作为您的主要选择。但是,这对于需要访问存储的不太常见的场景(例如更换减速器)可能很有用。

useSelector钩子接受一个函数,该函数确定我们要从存储中提取哪条数据,因此计数器变量,因为它是唯一存在的。通过使用这个钩子,react-redux 为组件创建了对Redux Store的订阅,以便在每次数据更改时自动更新。

import classes from './Counter.module.css';
import {useSelector} from "react-redux";

const Counter = () => {
    const counter = useSelector(state => state.counter)

    return (
        <main className={classes.counter}>
            <h1>Redux Counter</h1>
            <div className={classes.value}>{counter}</div>
        </main>
    );
};

export default Counter;

从组件调度操作

为了从组件内调度动作,react-redux 提供了另一个名为useDispatch的钩子。调用钩子时,不需要任何参数,因为它只返回一个可以在执行时发送动作的函数。在函数的执行中,现在可以提供一个对象,该对象包含与更新计数器的操作相关联的类型。

import classes from './Counter.module.css';
import {useDispatch, useSelector} from "react-redux";

const Counter = () => {
    const dispatch = useDispatch();
    const counter = useSelector(state => state.counter);

    const incrementHandler = () => {
        dispatch({type: "increment"});
    };

    const decrementHandler = () => {
        dispatch({type: "decrement"});
    };

    return (
        <main className={classes.counter}>
            <h1>Redux Counter</h1>
            <div className={classes.value}>{counter}</div>
            <div>
                <button onClick={incrementHandler}>Increment</button>
                <button onClick={decrementHandler}>Decrement</button>
            </div>
        </main>
    );
};

export default Counter;

将有效负载附加到操作

到目前为止,我们已经了解了如何发送简单的操作,但有时操作也可以有一个称为有效负载的附加值。在 dispatch 函数中,除了类型,我们可以随意添加和命名其他值。让我们为 reducer 函数创建一个新的案例场景,它需要一个额外的值,称为 amount。

// src/store/index.js
function counterReducer(state = {counter: 0}, action) {
    switch (action.type) {
       ...
        case 'increase':
            return {counter: state.counter + action.amount}
        ...
    }
}

在组件中,我们可以创建一个新的处理程序来调度这个带有额外“数量”值的新动作,并将它传递给一个新按钮。

// Component

import classes from './Counter.module.css';
import {useDispatch, useSelector} from "react-redux";

const Counter = () => {
    const dispatch = useDispatch();
    const counter = useSelector(state => state.counter);

    [...]

    const decrementHandler = () => {
        dispatch({type: "decrement"});
    };

    return (
        <main className={classes.counter}>
            <h1>Redux Counter</h1>
            <div className={classes.value}>{counter}</div>
            <div>
                <button onClick={incrementHandler}>Increment</button>
                <button onClick={decrementHandler}>Decrement</button>
                <button onClick={increaseHandler}>Increase by 3</button>
            </div>
        </main>
    );
};

export default Counter;

与多个全局状态一起工作

计数器状态可能不是我们想要处理的唯一全局状态。因此,让我们添加一个新的切换状态来使用几个全局状态来练习我们的计数器。添加新状态时,我们还必须在其他情况下处理它,否则我们可能会覆盖其他状态变量并破坏应用程序。

function counterReducer(state = initialState, action) {
    switch (action.type) {
        case 'increment':
            return {...state, counter: state.counter + 1}
        case 'decrement':
            return {...state, counter: state.counter - 1}
        case 'increase':
            return {...state, counter: state.counter + action.amount}
        case 'toggle':
            return {...state, showCounter: !state.showCounter}
        default:
            return state
    }
}

在组件内部,选择新的ShowCounter 状态并将其添加到 JSX 逻辑中:

import classes from './Counter.module.css';
import {useDispatch, useSelector} from "react-redux";

const Counter = () => {
    const dispatch = useDispatch();
    const {counter, showCounter} = useSelector(state => state);

    const toggleCounterHandler = () => {
        dispatch({type: "toggle"})
    };

    const incrementHandler = () => {
        dispatch({type: "increment"});
    };

    const increaseHandler = () => {
        dispatch({type: "increase", amount: 3});
    };

    const decrementHandler = () => {
        dispatch({type: "decrement"});
    };

    return (
        <main className={classes.counter}>
            <h1>Redux Counter</h1>
            {showCounter && <div className={classes.value}>{counter}</div>}
            <div>
                <button onClick={incrementHandler}>Increment</button>
                <button onClick={decrementHandler}>Decrement</button>
                <button onClick={increaseHandler}>Increase by 3</button>
            </div>
            <button onClick={toggleCounterHandler}>Toggle Counter</button>
        </main>
    );
};

export default Counter;

🥨 Redux 工具包

Redux Toolkit 是一个帮助开发人员使用 Redux 并防止错误的库。该包旨在成为编写 Redux 逻辑的标准方式,因此值得拥有自己的部分。您可以在Redux Tollkit 网站上找到更多信息。

安装:

# NPM
npm install @reduxjs/toolkit

# Yarn
yarn add @reduxjs/toolkit

Redux 库可以从 package.json 中删除,因为它已经包含在 reduxjs/toolkit 中。

创建切片

此功能允许开发人员创建不同的全局状态切片。如果我们有不直接相关的不同状态,例如身份验证和计数器状态,这将很有用。此外,我们可以在不同的文件中创建切片,以获得更易于管理的代码。 createSlice接受一个对象作为参数,我们可以在其中添加相关的countershowcounter逻辑。

每个切片都需要一个name、一个initialState和一个reducers对象。后者包含方法,每个方法都有一个名称并接收最新状态的副本。这样的结构可以让开发者停止编写 if/case 检查,减少代码的长度,使其更具可读性。

与之前的 reducer 方法不同,我们现在可以改变state,或者至少看起来如此!事实上,Redux ToolkitcreateSlice函数不会意外操作现有state,因为Redux Toolkit在内部使用另一个名为Immer的包,它克隆现有state,创建一个新的state对象,保存所有未编辑的状态并覆盖我们在 an不可变的方式(查看更多)。此功能使开发人员的工作更加轻松和安全,因为我们不再需要担心重写状态一遍又一遍地为每种方法。当需要有效负载时,我们也可以接受action参数并在方法中使用它。

这是我们新创建的切片的外观:

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

const initialState = {counter: 0, showCounter: true}

const counterSlice = createSlice({
    name: "counter",
    initialState,
    reducers: {
        increment(state) {
            state.counter++
        },
        decrement(state) {
            state.counter--
        },
        increase(state, action) {
            state.counter += action.amount
        },
        toggleCounter(state) {
            state.showCounter = !state.showCounter
        }
    }
});

连接 Redux 工具包状态

在我们的例子中,我们可以简单地将counterSlice.reducer作为 store 的参数传递,它会起作用:

const store = createStore(counterSlice.reducer)

但是,如果我们有多个切片,我们可以使用标准的 Redux 函数combineReducers甚至更好的是 Redux Toolkit 提供 的configureStore函数。configureStorecreateStore的替代品,但它使合并 reducer 更容易。它接受一个配置对象作为参数,它需要一个 reducer 属性。reducer 属性可以接受单个 reducer,或者,如果应用程序有多个切片,则可以接受包含要合并的所有 reducer 的对象。

const store = configureStore({
    reducer: counterSlice.reducer
})

#OR

const store = configureStore({
    reducer: {counter: counterSlice.reducer}
})

调度

createSlice自动为所有reducer创建标识符,要访问它们,我们可以简单地从sliceName.actions中选择它们。我们现在可以使用Redux Toolkit创建的方法,当调用该方法时,会创建已经具有type属性的action 对象,每个action都有一个唯一标识符。因此,作为开发人员,我们不再需要担心创建操作对象唯一标识符或避免拼写错误。现在我们可以导出文件底部的所有动作:

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

[...]

const store = configureStore({
    reducer: {counter: counterSlice.reducer}
})

export const counterActions = counterSlice.actions
export default store

回到 Counter 组件,我们可以导入这些操作并相应地重构我们的代码:

import {useDispatch, useSelector} from "react-redux";
import {counterActions} from "../store";

const Counter = () => {
    const dispatch = useDispatch();
/* Change useSelector(state => state)
 to useSelector(state => state.counter) */
    const {counter, showCounter} = useSelector(state => state.counter);

    const toggleCounterHandler = () => {
        dispatch(counterActions.toggleCounter())
    };

    const incrementHandler = () => {
        dispatch(counterActions.increment());
    };

    const increaseHandler = () => {
        dispatch(counterActions.increase(3));
    };

    const decrementHandler = () => {
        dispatch(counterActions.decrement());
    };

    [...]

对增加减速器的另一个小改动,现在默认情况下,数量变为有效负载

increase(state, action) {
            state.counter += action.payload
        },

多个切片的示例

向 React 应用程序添加更多切片时,存在创建过长文件的风险。可能值得将其拆分为更小的部分并为创建的每个切片创建一个文件。

// store/index.js

import {configureStore} from "@reduxjs/toolkit";
import counterSliceReducer from './counter'
import authSliceReducer from './auth'

const store = configureStore({
    reducer: {counter: counterSliceReducer, auth: authSliceReducer}
});

export default store;

// store/counter.js

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

const initialCounterState = {counter: 0, showCounter: true};

const counterSlice = createSlice({
    name: "counter",
    initialState: initialCounterState,
    reducers: {
        increment(state) {
            state.counter++
        },
        decrement(state) {
            state.counter--
        },
        increase(state, action) {
            state.counter += action.payload
        },
        toggleCounter(state) {
            state.showCounter = !state.showCounter
        }
    }
});
export const counterActions = counterSlice.actions;
export default counterSlice.reducer

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

const authInitialState = {isAuthenticated: false};

const authSlice = createSlice({
    name: "auth",
    initialState: authInitialState,
    reducers: {
        login(state) {
            state.isAuthenticated = true
        },
        logout(state) {
            state.isAuthenticated = false
        }
    }
});

export const authActions = authSlice.actions
export default authSlice.reducer

重构代码后不要忘记修复小的导入错误!

文章来源:https://pietropiraino.hashnode.dev/react-redux-redux-toolkit

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容