useReducer
在要管理的 state 變多之後,因為還會配上一個 setter,
這些 setter 通常又會有一些特定的邏輯要同時執行,就會包成一個函式,
整個元件可能就會開始膨脹而難以管理,這時候就可以考慮使用 useReducer
。
用法
狀態定義
假設原本有 data
、isLoading
這兩個 state:
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(true);
改為用 reducer 的方式管理後,這些 state 可以拉出來做成一個物件並設定初始值,
變數習慣命名會有 initial 前綴:
const initialStates = {
data: [],
isLoading: true,
};
原本用來改變狀態的 setter,會拉出來存到 reducer 這個變數,
來定義這些方法是給哪個狀態用的, 也建議改成用 switch
來分流要觸發的行為:
function reducer(state, action) {
switch (action.type) {
case 'SET_LOADING':
return { ...state, isLoading: action.payload };
case 'SET_DATA':
return { ...state, data: action.payload };
case 'RESET':
return initialStates;
default:
return state;
}
}
每個 case 跟原本的 setter 看起來差不多,
差別在於想要變更的值要透過 action.payload
取得,
也可以額外定義新的 action 來一次改變好幾個 state,
像是讓全部 state 都回到初始值的 reset 功能。
在元件中執行
狀態跟方法都被分別定義好之後,這時就可以呼叫 useReducer
,
把剛剛定義好的 state 與 reducer 傳入:
function MyComponent() {
const [state, dispatch] = useReducer(reducer, initialStates);
console.log(state); // 會印出 initialStates 裡面的東西
return (
<div>
{state.data.map((item) => (
<div>{item.name}</div>
))}
</div>
);
}
原本要使用 setter 觸發的狀態變更,要改用 dispatch
傳入對應格式,
type
就是剛剛在 reducer 裡面定好的 case,payload
就是傳入的值:
dispatch({ type: 'SET_LOADING', payload: false });
coding style
改變狀態的方式從 setter 改為 dispatch
後,
這樣的格式 { type: 'SET_LOADING', payload: false }
讓語法看起來變冗長了,
如果有要把方法傳遞下去時,最好再封裝成一個函式,
原本傳遞下去的 props 命名,也建議改為更語意化的 on 事件,
否則掛在 JSX 上的 callback 會很長也不好閱讀:
function handleChange() {
dispatch({ type: 'SET_LOADING', payload: false });
}
return <ChildComponent onChange={handleChange} />;
action type 目前全部都是用字串傳下去的,
如果字串有打錯的話第一時間找不到報錯,就很難抓到,
所以這些字串通常會拉出來做成可存取的物件:
const ACTION = {
SET_LOADING: 'SET_LOADING',
SET_DATA: 'SET_DATA',
};
// 改成變數後能降低出包率
dispatch({
type: ACTION.SET_LOADING,
payload: false,
});
也推薦在 reducer 的定義中加上錯誤提示:
const reducer = (state, action) => {
switch (action.type) {
case 'SET_LOADING':
return { ...state, isLoading: action.payload };
case 'SET_DATA':
return { ...state, data: action.payload };
case 'RESET':
return initialCompStates;
default: {
throw Error('Unknown action: ' + action.type);
}
}
};