通常当我们要设定state时,都是透过setState(要指定的值)
。但这样做有两个问题:
setState
的元件可以任意指定值给state
state
结构复杂、但我们又只有要修改其中部分值时,很容易出错。举例来说,这次有个铁人赛参赛者(我忘记在哪看到的了)提及他想要用这样的方式来处理资料:
const [data, setData] = useState({ A: a, B: b });
然後他想分别建立两个单独设定A
和B
的按键:
<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的修改不会被同事改错呢?
因为刚刚的问题在大型网站上常常出现,Facebook的开发者针对这点提出了Flux设计模式。这里我们不会详述Flux,不过简而言之就是当我们在做资料管理、流程设计时,不应该让别人能够随意修改,而是我们要预先定义好修改的规则,并让其他开发者透过这些规则来操作。
在Flux观念下,我们操作流程和资料的过程大概变成像这样:
由於React最通用的状态管理工具Redux(下一篇会讲它)是采用Flux结构,而在Redux中reducer跟store扮演的角色是一样的,所以我们这里放入说明的同一个地方。接下来的说明我们也会以Redux的架构为主
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>
现在,我们来试着让先前的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
上
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
即可:
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
被变成非布林值的状况。
>>: Android Studio - AlertDialog - 列表选单
Q1. 什麽是 SSRF? SSRF (Server Side Request Forgery),也...
嗨大家好,这系列的文章主要是想纪录我在写 Leetcode / AlgoExpert 的题目时的一些...
今天要来做一下粒子系统 首先要来了解 为什麽要了解 class生成物件呢 大量快速建立同类型的物件 ...
Odoo 的onchange 有些特别要注意的地方, 像是onchange会影响到的栏位都要写入XM...
什麽是好的网站设计? 使用者使用网站时是否容易操作及有良好的动线,避免过多不必要的元素,让使用者快速...