【Day.27】React进阶 - 用useReducer定义state的更动原则

通常当我们要设定state时,都是透过setState(要指定的值)。但这样做有两个问题:

  • 使用setState的元件可以任意指定值给state
  • state结构复杂、但我们又只有要修改其中部分值时,很容易出错。

举例来说,这次有个铁人赛参赛者(我忘记在哪看到的了)提及他想要用这样的方式来处理资料:

const [data, setData] = useState({ A: a, B: b });

然後他想分别建立两个单独设定AB的按键:

<button onClick={()=>{ setData({A: newA}) }}></button>
<button onClick={()=>{ setData({B: newB}) }}></button>

然而这样的写法是错的。因为useState给出来的setData函式并不会自动去修改物件中的单一属性,而是直接把你丢给setData的参数整个变成data新的值。以A为例,按下设定A的按键後,新的data不会是{ A: newA, B: b },而是{ A: newA }

最後,那位参赛者用ES6的spread operator展开原始的data物件,解决这个问题。

<button onClick={()=>{ setData({...data, A: newA}) }}></button>
<button onClick={()=>{ setData({...data, B: newB}) }}></button>

虽然这样做的确解决了他的case,但是如果物件资料变的很复杂呢?如果我们要修改的结构散布在物件各层呢? 要如何才能确保state的修改不会被同事改错呢?

action | reducer | dispatch

因为刚刚的问题在大型网站上常常出现,Facebook的开发者针对这点提出了Flux设计模式。这里我们不会详述Flux,不过简而言之就是当我们在做资料管理、流程设计时,不应该让别人能够随意修改,而是我们要预先定义好修改的规则,并让其他开发者透过这些规则来操作

在Flux观念下,我们操作流程和资料的过程大概变成像这样:

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

由於React最通用的状态管理工具Redux(下一篇会讲它)是采用Flux结构,而在Redux中reducer跟store扮演的角色是一样的,所以我们这里放入说明的同一个地方。接下来的说明我们也会以Redux的架构为主

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

useReducer

useReducer是React提供用来简易实现Flux架构的React hook,基本上它就是一个「能够预先定义state设定规则」的useState。

和useState不同的是,useReducer必须要接收两个参数。第一个是函式,要定义有哪些规则、规则对应的逻辑。第二个则是state的初始值。useReducer的语法为下:

const [state, dispatch] = useReducer(reducerFunc, initStateValue);

操作者可以透过dispatch函式传送参数:

dispatch({type: "ADD"})

当操作者呼叫dispatch後,reducerFunc会被呼叫并接收到两个参数。第一个是state先前的值,第二个则是操作者刚刚传入dispatch的参数。reducerFunc必须要接收一个回传值,这个回传值会变成state新的值:

const reducerFunc = function(state, action){
    // action get { type:"ADD" }
    switch(action.type){
        case "ADD":
            return state+1; // new State
        case "SUB":
            return state-1; // new State
        default:
            throw new Error("Unknown action");
    }
}

以上面的那位参赛者的状况来说,它可以改成这样:

const reducer = function(state, action){
    // 由於JS物件类似call by ref,先复制一份避免直接修改造成非预期错误
    const stateCopy = Object.assign({}, state);
    
    switch(action.type){
        case "SET_A":
            stateCopy.A = action.A;
            return stateCopy; // new State
        case "SET_B":
            stateCopy.B = action.B;
            return stateCopy; // new State
        default:
            throw new Error("Unknown action");
    }
}

之後只要这样使用,程序码就会更直观,也能避免不小心在哪个地方写错导致state被覆盖:

<button onClick={()=>{ dispatch({type: "SET_A", A: newA}) }}></button>
<button onClick={()=>{ dispatch({type: "SET_B", B: newB}) }}></button>

加入useReducer到我们的程序中吧

现在,我们来试着让先前的isOpen改成用useReducer改变。

在src/page/MenuPage.js中,先定义reducer:

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

接着引入useReducer,并在元件中取得state和dispatch

import React, { useState, useReducer,useMemo,useEffect} from 'react';
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);

然後绑定在本来的Context的setOpenContext

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

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

let menuItemWording=[
    "Like的发问",
    "Like的回答",
    "Like的文章",
    "Like的留言"
];

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, setMenuItemData] = useState(menuItemWording);
    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={()=>{
                let menuDataCopy = ["测试资料"].concat(menuItemData);
                setMenuItemData(menuDataCopy); 
            }}>更改第一个menuItem</button>
        </OpenContext.Provider>
    );
}

export default MenuPage;

最後让呼叫dispatch的Menu不是直接传isOpen新的值,而是传入要使用的type:SWITCH即可:

  • src/component/Menu.js
import React, {useContext, useMemo} from 'react';
import { OpenContext } from '../context/ControlContext';

const menuContainerStyle = {
    position: "relative",
    width: "300px",
    padding: "14px",
    fontFamily: "Microsoft JhengHei",
    paddingBottom: "7px",
    backgroundColor: "white",
    border: "1px solid #E5E5E5",
};

const menuTitleStyle = {
    marginBottom: "7px",
    fontWeight: "bold",
    color: "#00a0e9",
    cursor: "pointer",
};

const menuBtnStyle = {
    position: "absolute",
    right: "7px",
    top: "33px",
    backgroundColor: "transparent",
    border: "none",
    color: "#00a0e9",
    outline: "none"
}

function Menu(props){
    const isOpenUtil = useContext(OpenContext);
    return (
        <div style={menuContainerStyle}>
            <p style={menuTitleStyle}>{props.title}</p>
            <button style={menuBtnStyle} onClick={
                ()=>{isOpenUtil.setOpenContext({type: "SWITCH"})}}>
                {(isOpenUtil.openContext)?"^":"V"}
            </button>
            <ul>{props.children}</ul>
        </div>
    );
}

export default Menu;

这样我们就能保护isOpen,不会哪天出现isOpen被变成非布林值的状况。


<<:  [第27天]30天搞懂Python-序列化

>>:  Android Studio - AlertDialog - 列表选单

【第十五天 - SSRF】

Q1. 什麽是 SSRF? SSRF (Server Side Request Forgery),也...

Leetcode/AlgoExpert 解题笔记 – Array 篇 (1)

嗨大家好,这系列的文章主要是想纪录我在写 Leetcode / AlgoExpert 的题目时的一些...

Day 20 - 物件导向与向量 - Class 粒子系统

今天要来做一下粒子系统 首先要来了解 为什麽要了解 class生成物件呢 大量快速建立同类型的物件 ...

Day 19 Onchange v.s. readonly

Odoo 的onchange 有些特别要注意的地方, 像是onchange会影响到的栏位都要写入XM...

如何制作一个精美的网站

什麽是好的网站设计? 使用者使用网站时是否容易操作及有良好的动线,避免过多不必要的元素,让使用者快速...