(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問
通常當我們要設定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
被變成非布林值的狀況。