useReducer & Reducers

隨著開發的功能和複雜度的逐漸提升,一個功能可能會由多組 State 來控制操作狀態,但因為 State 的更新機制,在某些情況下可能造成維護不易或預料之外的 Bug,所以 React 提供了另一個 Hook - useReducer() 來解決這種情況下的問題

如果是一個簡單的表單驗證功能,透過 useState() 會需要個別有一組資料和驗證加以管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const Login = props => {
const [enteredEmail, setEnteredEmail] = useState("");
const [emailsValid, setEmailsValid] = useState("");
...

const emailChangeHandler = event => {
setEnteredEmail(event.target.value);
};

const validateEmailHandler = () => {
setEmailsValid(enteredEmail.includes("@"));
};

const submitHandler = event => {
event.preventDefault();
...
};

return (
<form onSubmit={submitHandler}>
<div
className={`${classes.control} ${
emailIsValid === false ? classes.invalid : ""
}`}
>
<label htmlFor="email">E-Mail</label>
<input
type="email"
id="email"
value={enteredEmail}
onChange={emailChangeHandler}
onBlur={validateEmailHandler}
/>
</div>
...
</form>
);
};

useReducer()

在上述的範例中,可以發現其實這兩個 State 可以看作為一組,因為驗證的 State 還是會透過資料的 State 來判斷以回應對應的值,這種情況就可以透過 useReducer() 將之改寫為一組 State

首先,介紹一下其結構,在前方回傳的變數其實和 useState() 相同,分別為 State 和修改 State 的 dispatchFn,比較特別的呼叫 useReducer() 所帶入的三個參數

1
const [state, dispatchFn] = useReducer(reducerFn, initialState, initFn);
  • reducerFn(prevState, action):
    因為其中的值並不會和 component function 內的其他值有交互作用,所以甚至可以把這個 function 寫在 component function 之外
    • prevState: 顧名思義就是”前一個”最新的 State snapshot
    • action: 藉由 dispatchFn 取得最新的 State snapshot
  • initialState: State 初始值
  • initFn: 將 State 設為初始值

透過 useReducer(),我們可以讓 State 為一個 object,並有兩個 key 分別表示值和驗證狀態,呼叫 dispatchFn() 時,可以單純帶入值和行為類別,將邏輯由 reducerFn 來處理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
const emailReducer = (prevState, action) => {
if (action.type === "USER_INPUT") {
return { value: action.val, isValid: action.val.includes("@") };
} else if (action.type === "INPUT_BLUR") {
return { value: prevState.value, isValid: prevState.value.includes("@") };
}

return { value: "", isValid: false };
};

const Login = props => {
const [emailState, dispatchEmail] = useReducer(emailReducer, {
value: "",
isValid: false
});
...

const emailChangeHandler = event => {
dispatchEmail({
type: 'USER_INPUT',
val: event.target.value
});
};

const validateEmailHandler = () => {
dispatchEmail({
type: 'INPUT_BLUR',
val: emailState.value
});
};

...

return (
<form onSubmit={submitHandler}>
<div
className={`${classes.control} ${
emailState.isValid === false ? classes.invalid : ""
}`}
>
<label htmlFor="email">E-Mail</label>
<input
type="email"
id="email"
value={emailState.value}
onChange={emailChangeHandler}
onBlur={validateEmailHandler}
/>
</div>
...
</form>
);
};

但這裡要注意的是,因為 isValid 初始值就是 false,所以一開始在 UI 上就會顯示驗證未通過的樣式,為了區分初始狀態,可已透過其他 falsy 值如 null, undefined 來代替

1
2
3
4
const [emailState, dispatchEmail] = useReducer(emailReducer, {
value: "",
isValid: null or undefined
});

With useEffect()

有了個別欄位的驗證變數後,仍然會需要一組總表單的驗證變數來判斷每個欄位是否都驗證通過,這時候就可以透過先前的 useEffect() 來監聽所有欄位的變化

1
2
3
4
5
6
7
8
9
10
11
useEffect(() => {
const identifier = setTimeout(() => {
setFormIsValid(
emailState.isValid && passwordState.isValid
);
}, 500);

return () => {
clearTimeout(identifier);
};
}, [emailState, passwordState]);

寫到這裡其實已經達到驗證功能的目的了,但還可以進一步優化的地方是,這樣的寫法在驗證上”過於頻繁”了,只要欄位的值一改動,就會進行監聽,其實需要判斷的時機只有在個別欄位的驗證結果發生改變時,才會需要更新總表單的驗證結果

1
2
3
4
5
6
7
8
9
10
11
useEffect(() => {
const identifier = setTimeout(() => {
setFormIsValid(
emailState.isValid && passwordState.isValid
);
}, 500);

return () => {
clearTimeout(identifier);
};
}, [emailState.isValid, passwordState.isValid]);

或是可以透過 ES6 object de-structuring 來個別取的 isValid 的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const { isValid: emailValid } = emailState;
const { isValid: passwordValid } = passwordState;

useEffect(() => {
const identifier = setTimeout(() => {
setFormIsValid(
emailValid && passwordValid
);
}, 500);

return () => {
clearTimeout(identifier);
};
}, [emailValid, passwordValid]);

useState() vs useReducer()

在 React 中,其實並沒有明確規定什麼時候該用哪一種 hook,兩者可以辦到的事相同,只是在什麼時機更適合,基本上可以為兩者的使用時機做出一些區別

  • useState()

    • 一個主要由 State 來管理的工具
    • 每個 State 獨立運作,較沒有相互作用的情況
    • State 的更新較為單純,改別的值也比限制在固定的範圍內
  • useReducer()

    • 更複雜的資料結構,如 object
    • 資料彼此之間的關聯性高,更新時環環相扣
    • 處理更複雜的邏輯

資料參考

React - The Complete Guide (Incl Hooks, React Router, Redux)
useReducer()
with useEffect()