ItIron2023
react
我們昨天看了一個有趣的useState問題,了解到initialValue的一些限制,今天我們會再看一個與state管理有關的例子,而它會稍稍有些複雜,還請見諒!我們馬上開始吧!
請觀察一下這個codesandbox以及下方的截圖。
今天你設計了一個表單,這個表單稍稍有些複雜,其中部分的欄位彼此相關,舉個例子來說你需要填寫Current Job的欄位後,Next Job這個可選填的欄位才會開啟,同時僅有當Name, Degree & Current Job這些必填欄位都填寫後,送出按鈕才可以點擊。
我們之前有個例子是利用一個大物件做狀態管理,這樣你就不用寫一堆state,你也吸取了上次的經驗完成了這次的需求,功能上並沒有任何問題,唯一的小小問題就是在於由於欄位之間有一些依賴關係,在你每一次更新state的同時都需要透過一個useEffect去檢查是否應該開啟Next Job以及Submit,在管理上就會稍微困難一些,請觀察以下的程式碼並試著優化相關的邏輯。
import React, { useState, useEffect } from "react";
function ComplexForm() {
const [state, setState] = useState({
personalInfo: { name: "", age: 0 },
education: { degree: "", isValid: false },
employment: { currentJob: "", nextJob: "", isValid: false },
isSubmitEnabled: false
});
useEffect(() => {
const { personalInfo, education, employment } = state;
const newIsSubmitEnabled =
personalInfo.name && education.isValid && employment.isValid;
if (state.isSubmitEnabled !== newIsSubmitEnabled) {
setState((prevState) => ({
...prevState,
isSubmitEnabled: newIsSubmitEnabled
}));
}
}, [state]);
const updatePersonalInfo = (name, age) => {
setState((prevState) => ({
...prevState,
personalInfo: { name, age }
}));
};
const updateEducation = (degree, isValid) => {
setState((prevState) => ({
...prevState,
education: { degree, isValid }
}));
};
const updateEmployment = (currentJob, nextJob, isValid) => {
setState((prevState) => ({
...prevState,
employment: { currentJob, nextJob, isValid }
}));
};
return (
<div>
<h1>useState with complex form</h1>
<div>
<label>Name: </label>
<input
type="text"
onChange={(e) =>
updatePersonalInfo(e.target.value, state.personalInfo.age)
}
/>
</div>
<div>
<label>Degree: </label>
<select
onChange={(e) =>
updateEducation(e.target.value, e.target.value !== "")
}
>
<option value="">Select</option>
<option value="Bachelors">Bachelors</option>
<option value="Masters">Masters</option>
</select>
</div>
<div>
<label>Current Job: </label>
<input
type="text"
onChange={(e) =>
updateEmployment(
e.target.value,
state.employment.nextJob,
e.target.value !== ""
)
}
/>
</div>
<div>
<label>Next Job: </label>
<input
type="text"
disabled={!state.employment.currentJob}
onChange={(e) =>
updateEmployment(
state.employment.currentJob,
e.target.value,
e.target.value !== ""
)
}
/>
</div>
<button disabled={!state.isSubmitEnabled}>Submit</button>
</div>
);
}
export default ComplexForm;
這個題目有著不止一種以上的解法,其中完全不要去更動其實也是一種選擇,畢竟它現階段並沒有真的造成什麼大問題,不過往往這類有相依關係的複雜state,光靠一個useState並配合物件作為initialValue會稍嫌有些吃力,畢竟你每一次的更動都要去考慮相依的值,不但在程式碼上需要額外的處理,最重要的是往往會讓你的程式碼可讀性變低,比方說像是這次範例中的useEffect就會讓人看起來有些許困惑。這類的情況一般來說我會建議採用useReducer,一個特別適用於複雜state管理的hook,尤其在state間有彼此相依的情況出現時。老樣子,如果你沒有聽過這玩意,我最後會留連結讓你去補課,我這邊只會跟你說為什麼我會採用這個解法以及我會怎麼用,上方的程式碼用useReducer改寫後會變為類似這樣。
基本上我們建立了一模一樣的初始state值,差別在於我們給予每一個操作對應的action,讓你的程式碼看起來稍稍乾淨一點,同時我們將validate的邏輯放在每一次的state更新中處理而不是另外讓一個useEffect來幫忙!一旦有這樣的結構出現,後續你需要修改或增加新的邏輯處理往往會方便不少!這麼一來未來要做維護也會輕鬆一些!
const initialState = {
personalInfo: { name: '', age: 0 },
education: { degree: '', isValid: false },
employment: { currentJob: '', nextJob: '', isValid: false },
isSubmitEnabled: false,
};
function formReducer(state, action) {
let updatedState = { ...state };
switch (action.type) {
case 'UPDATE_PERSONAL_INFO':
updatedState.personalInfo = action.payload;
break;
case 'UPDATE_EDUCATION':
updatedState.education = action.payload;
break;
case 'UPDATE_EMPLOYMENT':
updatedState.employment = action.payload;
break;
default:
return state;
}
// Run the validation logic every time state updates
const { personalInfo, education, employment } = updatedState;
updatedState.isSubmitEnabled = personalInfo.name && education.isValid && employment.isValid;
return updatedState;
}
function ComplexForm() {
const [state, dispatch] = useReducer(formReducer, initialState);
const updatePersonalInfo = (name, age) => {
dispatch({
type: 'UPDATE_PERSONAL_INFO',
payload: { name, age },
});
};
const updateEducation = (degree, isValid) => {
dispatch({
type: 'UPDATE_EDUCATION',
payload: { degree, isValid },
});
};
const updateEmployment = (currentJob, nextJob, isValid) => {
dispatch({
type: 'UPDATE_EMPLOYMENT',
payload: { currentJob, nextJob, isValid },
});
};
return (
<div>
<h1>Complex Form</h1>
<div>
<label>Name: </label>
<input
type="text"
onChange={(e) => updatePersonalInfo(e.target.value, state.personalInfo.age)}
/>
</div>
<div>
<label>Degree: </label>
<select
onChange={(e) => updateEducation(e.target.value, e.target.value !== '')}
>
<option value="">Select</option>
<option value="Bachelors">Bachelors</option>
<option value="Masters">Masters</option>
</select>
</div>
<div>
<label>Current Job: </label>
<input
type="text"
onChange={(e) => updateEmployment(e.target.value, state.employment.nextJob, e.target.value !== '')}
/>
</div>
<div>
<label>Next Job: </label>
<input
type="text"
disabled={!state.employment.currentJob}
onChange={(e) => updateEmployment(state.employment.currentJob, e.target.value, e.target.value !== '')}
/>
</div>
<button disabled={!state.isSubmitEnabled}>Submit</button>
</div>
);
}
export default ComplexForm;
我們今天看了一個useState v.s useReducer的情境,坦白說這一直都是個許多人爭吵的議題,在絕大多數的情境下useState就足以解決了,尤其當你用useState管理一個大物件時,許多人甚至說不出這與useReducer差在哪邊。不過就像之前在講useLayoutEffect時提過的,只要你能認清你要處理的情況,有些工具就確實能好好的幫上忙。useReducer在處理複雜的state管理時確實相當出色,也經常配合context去使用,如果你對這玩意不熟悉的話我建議你稍微補一下官網的課,相信我他總有一天會幫上忙的!我們明天見囉,終於要進入面試情境題了!
本文章同步發布於個人部落格,有興趣的朋友也可以來逛逛~!