【Day.28】React进阶 - 导入Redux,让元件沟通更简洁

当专案中的阶层变复杂,state和props变的很多,资料在多层component之间的传递也越来越多。产生了一堆纯粹用来传递用的props和父component。

工程师心想: 「有没有一个全局的state和setState可以让所有的元件共同操作呢?」

於是Global State的概念就诞生了。

Global State的概念就像是住宅大厦的公共设施,它不单独属於任何一个人,也能够被任何人取用。

【Day.17】React入门 - 利用useContext进行多层component沟通中,我们提及了React本身提供的状态管理API。但是我们也在後续的文章中讲到了Context API本身并不是设计拿来做大型专案的状态管理,因而存在麻烦的效能问题。

现在,就让我们来认识最被广为使用的状态管理套件 - Redux。

Redux 的由来

Redux在2015年诞生。他不只是一个普通的全局state和setState工具而已,Redux受到了Facebook提出的设计概念Flux启发。有关Flux诞生的原因,我们在【Day.27】React进阶 - 用useReducer定义state的更动原则 有说明。

总之,我们不应该让别人能够随意修改state,而是要预先定义好修改的规则,并让其他开发者透过这些规则来操作。

同时,我们也说过在Flux观念下,我们操作state的过程大概变成像这样:

  1. 管理者预先定义好有哪些规则(action)可以使用
  2. 管理者预先定义好规则(action)对应到的逻辑运算(store)是什麽。
  3. 操作者透过一个沟通用的函式(dispatch),把他选择的规则(action)和需要的参数(payload)丢给管理者
  4. 流程/资料透过管理者规定好的方式更新

上图截自Facebook对於flux的说明影片

Redux的运作流程

Redux基於上述Flux的架构外,又做了一些补充和修改

  1. 管理者预先定义好有哪些state可以使用,并采用Single source,让所有人拿到的state是一样、共用的。
  2. 管理者预先定义好有哪些修改规则(action)可以使用
  3. 管理者预先定义好规则(action)对应到的逻辑运算(reducer)是什麽。
  4. Redux把所有state和对应的reducer包成一起,称为store
  5. 透过一个Provider把store提供给专案中所有的元件

而操作者可以有两种操作选择:

  1. 操作者可以透过一个selector,从store里面取出想要的state
  2. 操作者可以透过一个沟通用的函式(dispatch),把他选择的规则(action)和需要的参数(payload)丢给管理者

依据上述流程,我们就能在任何地方取得state,同时state也会透过管理者规定好的方式更新。


上图引用自Redux官方文件

Redux使用

1. 安装

首先,请先打开terminal,输入

npm install --save redux react-redux 

Redux和专为React打造的react-redux就会被安装。

2. 设定action和定义reducer

请在src底下新增model资料夹,并在里面建立reducer.js

useReducer一样,当操作者呼叫dispatch後,Redux会呼叫Reducer函式。Reducer函式的语法是:

  • 接收两个参数
    • 第一个是state之前的值
    • 第二个则是操作者传入dispatch函式的参数。
  • Reducer必须要有一个回传值,该值会变成state新的值。

请注意第一次执行的时候,reducer的第一个参数(state)如果有给default value,该default value就会变成state的初始值。

const initState = {
    menuItemData: [
        "Like的发问",
        "Like的回答",
        "Like的文章",
        "Like的留言"
    ],
  };

const itemReducer = (state = initState, action) => {
    switch (action.type) {
      case 'ADD_ITEM': {
        const menuItemCopy = state.menuItemData.slice();
        return { menuItemData: [action.payload.itemNew].concat(menuItemCopy) };
      }
      case 'CLEAN_ITEM': {
        return { menuItemData: [] };
      }
      default:
        return state;
    }
};

export {itemReducer};

而虽然我们这里是直接把action字串定义在reducer中,但比较好的方式应该是让action字串也用变数来管理,并用该变数来判断action:

const ADD_ITEM = 'ADD_ITEM';
const CLEAN_ITEM = 'CLEAN_ITEM';

3. 用store包覆action和reducer

在src/model底下新增store.js

要创立store的话,必须要使用redux提供的APIcreateStore

import {createStore} from "redux";

接着引入刚刚定义好的reducer,丢给createStore就能产生store了

  • src/model/store.js
import {createStore} from "redux";
import {itemReducer} from "./reducer.js";

const itemStore = createStore(itemReducer); 

export {itemStore};

createStore还可以吃一些middleware参数,帮redux多加一些功能,下一篇我们会提。

如果你有多个reducer要包起来,可以使用combineReducers这个API

import {createStore} from "redux";
import {itemReducer, otherReducer} from "./reducer.js";

const store = createStore(combineReducers(
    itemReducer,
    otherReducer 
)); 

export {itemStore};

4. 使用Provider包覆所有元件

Provider是react-redux提供的特殊React元件,被<Provider></Provider>包住的元件都能恣意取用store里面的state。它的语法是

<Provider store={store}> 

</Provider>

现在,我们回到所有React程序的起点,引入Provider和刚刚建立的itemStore,用它包住所有程序。

  • src/index.js
import { Provider } from "react-redux";
import { store } from "./model/store.js";

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

我个人会习惯把Provider放在App内的最外层,但这里我觉得这样示意对新手比较好理解。

接着就能在里面的元件取用state了。但在function component使用Redux的方式会比较特别,必须要使用Redux提供的hook

使用useSelector取得state

React-Redux提供了一个hookuseSelector,能让我们在React function component中选取想要从Redux取得的state。

import { useSelector } from 'react-redux';

useSelector本身需要一个参数,此参数为函式,定义了你要如何从所有state中挑选你需要的state。

例如,由於刚刚我们定义的state结构为:

{
    menuItemData: [
        "Like的发问",
        "Like的回答",
        "Like的文章",
        "Like的留言"
    ],
};

useSelector会把所有的state丢入我们定义的函式参数中,我们取得menuItemData的方式就是从参数函式把它单独取出并回传:

const menuItemData = useSelector(state => state.menuItemData);

menuItemData变数就会是我们需要的state。

使用useDispatch取得dispatch

React-Redux提供的另一个hookuseDispatch,能让我们在React function component中呼叫dispatch函式。

import { useDispatch } from 'react-redux';

使用上很简单很单纯,先把这个函式取出来:

const dispatch = useDispatch();

然後想要更动state时直接呼叫它就可以。呼叫dispatch时记得要传「想要选择的更动规则、想要传的参数」。 详细你可以回头去看刚刚的reducer是怎麽定义的

  • 新增item
dispatch({
    type: "ADD_ITEM",
    payload: {itemNew:"测试资料"}
}); 
  • 清空item
dispatch({ type: "CLEAN_ITEM" }); 

加入Redux到我们的程序码吧

store、reducer那些的实作跟上面的范例完全一样,我就不再写一次了。这里我只有实作把先前的menuItemData搬到Redux後,如何在MenuPage引入的作法,你可以自己试试看如何把isOpen搬进Redux。

  • src/page/MenuPage.js
import React, { useReducer,useMemo,useEffect} from 'react';

import MenuItem from '../component/MenuItem';
import Menu from '../component/Menu';
import { OpenContext } from '../context/ControlContext';

import { useSelector, useDispatch } from 'react-redux';

const reducer = function(state, action){
    switch(action.type){
        case "SWITCH":
            return !state;
        default:
            throw new Error("Unknown action");
    }
}

const MenuPage = () =>{
    const [isOpen, isOpenDispatch] = useReducer(reducer,true);

    const menuItemData = useSelector(state => state.menuItemData);
    const dispatch = useDispatch();

    let menuItemArr = useMemo(()=>menuItemData.map((wording) => <MenuItem text={wording} key={wording}/>),[menuItemData]);

    return (
        <OpenContext.Provider value={{ 
            openContext: isOpen, 
            setOpenContext: isOpenDispatch
        }} >
            <Menu title={"Andy Chang的like"}>
                {menuItemArr}
            </Menu>
            <button onClick={()=>{
                dispatch({
                    type: "ADD_ITEM",
                    payload: {itemNew:"测试资料"}
                }); 
            }}>更改第一个menuItem</button>
        </OpenContext.Provider>
    );
}

export default MenuPage;

Redux家族

因为Redux本身还不够用,近年来又衍伸出了各式各样的Redux版本和middleware,例如:

  • Redux-Actions

    把redux的流程封装简化

  • Redux-Saga

    着重於redux的非同步处理

  • Redux-Thunk

    把redux的非同步处理再更简化

  • Redux-Observable

    以functional-programming的方式处理资料流

下一篇我们就会来聊如何使用Redux-Thunk。


<<:  Day 28 - 重要的钥匙要藏好

>>:  出生第35天 变态四乾妈

Day 10. 实实在在的实体 - Vue Instance

Vue Instance 实体/实例 Vue Instance可以解释成实体化一个Vue的物件,每个...

Day 18 - Android Studio 如何切换Activity(分页)

Day 18 - Android Studio 如何切换Activity(分页) 昨天我们讲了如何使...

Day12-救世主Promise

前言 在ES6之前,要使用非同步并且正确输出资料必须透过callback,然而callback不好学...

[Day28] HTB Optimum

URL : https://app.hackthebox.eu/machines/6 IP : 1...

Day 20 To Do List - 封装

第 20 天~ 走到了三分之二了~!!! 好激动阿!! 再接再厉~ 加油! 昨天把 To Do Li...