(2024/04/06更新) 因應React在18後更新了許多不同的語法,更新後的教學之後將陸續放在 新的blog 中,歡迎讀者到該處閱讀,我依然會回覆這邊的提問
當專案中的階層變複雜,state和props變的很多,資料在多層component之間的傳遞也越來越多。產生了一堆純粹用來傳遞用的props和父component。
工程師心想: 「有沒有一個全局的state和setState可以讓所有的元件共同操作呢?」
於是Global State的概念就誕生了。
Global State的概念就像是住宅大廈的公共設施,它不單獨屬於任何一個人,也能夠被任何人取用。
在【Day.17】React入門 - 利用useContext進行多層component溝通中,我們提及了React本身提供的狀態管理API。但是我們也在後續的文章中講到了Context API本身並不是設計拿來做大型專案的狀態管理,因而存在麻煩的效能問題。
現在,就讓我們來認識最被廣為使用的狀態管理套件 - Redux。
Redux在2015年誕生。他不只是一個普通的全局state和setState工具而已,Redux受到了Facebook提出的設計概念Flux啟發。有關Flux誕生的原因,我們在【Day.27】React進階 - 用useReducer定義state的更動原則 有說明。
總之,我們不應該讓別人能夠隨意修改state,而是要預先定義好修改的規則,並讓其他開發者透過這些規則來操作。
同時,我們也說過在Flux觀念下,我們操作state的過程大概變成像這樣:
Redux基於上述Flux的架構外,又做了一些補充和修改
而操作者可以有兩種操作選擇:
依據上述流程,我們就能在任何地方取得state,同時state也會透過管理者規定好的方式更新。
上圖引用自Redux官方文件
首先,請先打開terminal,輸入
npm install --save redux react-redux
Redux和專為React打造的react-redux就會被安裝。
請在src底下新增model資料夾,並在裡面建立reducer.js
和useReducer
一樣,當操作者呼叫dispatch後,Redux會呼叫Reducer函式。Reducer函式的語法是:
請注意第一次執行的時候,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';
在src/model底下新增store.js
要創立store的話,必須要使用redux提供的APIcreateStore
import {createStore} from "redux";
接著引入剛剛定義好的reducer,丟給createStore就能產生store了
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};
Provider是react-redux提供的特殊React元件,被<Provider></Provider>
包住的元件都能恣意取用store裡面的state。它的語法是
<Provider store={store}>
</Provider>
現在,我們回到所有React程式的起點,引入Provider
和剛剛建立的itemStore
,用它包住所有程式。
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。
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。
React-Redux提供的另一個hookuseDispatch
,能讓我們在React function component中呼叫dispatch
函式。
import { useDispatch } from 'react-redux';
使用上很簡單很單純,先把這個函式取出來:
const dispatch = useDispatch();
然後想要更動state時直接呼叫它就可以。呼叫dispatch時記得要傳「想要選擇的更動規則、想要傳的參數」。 詳細你可以回頭去看剛剛的reducer是怎麼定義的
dispatch({
type: "ADD_ITEM",
payload: {itemNew:"測試資料"}
});
dispatch({ type: "CLEAN_ITEM" });
store、reducer那些的實作跟上面的範例完全一樣,我就不再寫一次了。這裡我只有實作把先前的menuItemData搬到Redux後,如何在MenuPage引入的作法,你可以自己試試看如何把isOpen
搬進Redux。
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版本和middleware,例如:
把redux的流程封裝簡化
著重於redux的非同步處理
把redux的非同步處理再更簡化
以functional-programming的方式處理資料流
下一篇我們就會來聊如何使用Redux-Thunk。
請問用了 redux 以後,經驗上來說會把所有的 state 都放到 redux 管理嗎?
有些 state 只會在單一 component 中使用,不知道這種是否也要使用 redux 管理?
通常是會需要和其他component共用的state才會放在redux。對於純state、context、redux的使用時機通常如下:
useReducer
。請注意這邊關於Context的敘述,由於在年初時React官方repo開啟了useContextSelector的PR,未來可能還會有變化。
總結來說這些都是大概的方向,不管是未來或是現在的專案,都可能會視遇到的情境、技術的更新,而做適當的調整。沒有絕對。
目前查了一些文章,你的Flux與Redux概念講的是最清楚的,真的很感激您。
(不知道為何查詢"react Flux"中文的文章超少,且內容我都看不太懂)