[React] 在子 component 觸發父 component 資料更新、深入理解 batch update 與 updater function
《React 思維進化》 筆記系列
1. [React] DOM, Virtual DOM 與 React element
2. [React] 了解 JSX 與其語法、畫面渲染技巧
3. [React] 單向資料流介紹以及 component 初探
4. [React] 認識狀態管理機制 state 與畫面更新機制 reconciliation
5. [React] 在子 component 觸發父 component 資料更新、深入理解 batch update 與 updater function
6. [React] 了解 immutable state 與 immutable update 方法
7. [React] 認識 component 的生命週期、了解每次 render 都有自己版本的資料
8. [React] React 中的副作用處理、初探 useEffect
9. [React] 對 hooks 的 dependencies 誠實,以維護資料流的連動
10. [React] React 18 effect 函式執行兩次的原因及 useEffect 常見情境
11. [React] 認識 useCallback、useMemo,了解 hooks 運作原理
前言
承接上篇 [React] 認識狀態管理機制 state 與畫面更新機制 reconciliation,此篇主要敘述的是《React 思維進化》 3–1 ~ 3–2 章節的筆記,若有錯誤歡迎大家回覆告訴我~
如何在子 component 裡觸發更新父 component 的資料
初學 React 時,可能會困惑要如何「在子 component 裡觸發更新父 component 的資料」,並同時維持單向資料流的一致性,本章就來探討如何達成這樣的需求~
以結論來說,React 沒有設計任何針對子 component 向上溝通或可更新父 component 資料的專門機制。但在實務應用時,我們經常會切分 component 來提高前端應用的可讀性和可維護性,而此時就可能會遇到將 state 定義在父 component,實際觸發資料更新的行為卻定義在子 component 的狀況,如果 React 沒有專門機制或規則,那我們要如何在子 component 觸發更新父 component 的資料?
要瞭解如何達到此需求,首先需要理解幾個 React 的運作機制:
Props 是唯讀且不可被修改的
在 [React] 單向資料流介紹以及 component 初探 這篇文章中有提到,props 是父 component 向子 component 傳遞資料的一種方式,而為維護單向資料流可靠性,開發者不可在子 component 內修改 props,因此若我們希望從子 component 修改父 component 資料,直接修改 props 是不可行的。
setState
方法可透過 props 傳遞
在 [React] 認識狀態管理機制 state 與畫面更新機制 reconciliation 這篇文章有提到,setState
方法是更新 state 值並觸發 re-render 的唯一合法手段,因此若要在子 component 觸發父 component 資料更新,我們要能在子 component 取得父 component 定義的 state 所對應的 setState
方法,再藉由呼叫 setState
方法來觸發父 component 的 re-render。
子 component 要如何取得父 component 的 setState
方法?我們可透過 props 來傳遞,因為 props 可傳遞任何資料型別的資料,而 setState
作為一個 JavaScript 函式,當然也可透過 props 來傳遞。示意圖如下。
以一個範例來說明,我們規劃在父 component Counter 定義 count state 並顯示,在子 component CounterControls 處理使用者操作與觸發資料更新的畫面區塊與運作邏輯。
而如果要在事件處理中觸發更新 count 的 state 值,子 component 就要想辦法拿到父 component (Counter component) 的 count 的setState
方法。
子 component 如何拿到父 component 的資料? 將父 component 的值傳遞給子 component 的合法手段有二,分別為 props 與 context,其中,props 是相對主要的手段,因此我們可透過 props 來傳遞 setState
方法,或任何經過封裝、有包含呼叫 setState
方法的函式,子 component 就可從 props 拿到 setState
方法並呼叫、觸發資料更新。
程式碼如下,父 component 會定義 state 並透過 props 傳遞 setCount
方法 給子 component:
import { useState } from "react";
import CounterControls from "./CounterControls";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p> Counter value: {count}</p>
{/* 將用來更新 count state 的 setCount 方法透過 props 傳遞給子 component */}
<CounterControls count={count} setCount={setCount} />
</div>
);
}
子 component 則透過 props 拿到 setCount
方法,就可在需要觸發資料更新時呼叫它:
export default function CounterControls({ count, setCount }) {
//在子 component 裡的事件處理呼叫從 props 傳遞下來的 setCount 方法
const decrement = () => setCount(count - 1);
const increment = () => setCount(count + 1);
const reset = () => setCount(0);
return (
<div>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
<button onClick={reset}>reset</button>
</div>
);
}
呼叫 setState
方法時觸發 re-render 的 component 是固定的
無論 setState
方法在哪裡呼叫,都可正常更新它對應的 state 資料,觸發該 state 所屬的 component 進行 re-render。所謂「無論在哪裡呼叫 setState
」代表不管你是在原本定義 component 的地方呼叫、傳遞到其他 component 呼叫、傳遞到 React 以外的環境呼叫、到外太空呼叫🚀(?…可能不行在外太空因為它不知道 setState
是什麼...),不管在哪呼叫,都一樣會觸發對應的 state 更新、和所屬的 component 的 re-render。
承上,如果將 setState
方法從父 component 傳給子 component 或孫代 component 去呼叫,最後會觸發 re-render 的 component 是?
答案:仍固定是 setState
方法對應的 state 所屬的 component re-render。如果是在父 component 定義該 state,就是觸發該父 component 的 re-render,而父 component re-render 時,也會連帶 re-render 其子component (在此篇文章有提到)。示意圖如下。
回到前面的 Counter 和 CounterControls 範例,我們在 Counter component 透過 props 傳遞 setCount
方法給子 component CounterControls,當我們在 CounterControls 呼叫 setCount
方法時,觸發的 state 更新和 re-render 的 component 會是?
答案:仍固定會觸發 Counter 的 re-render,而 Counter re-render 也會連帶 re-render 子 component CounterControls。
React 不存在且也不需要「針對子 component 向上溝通父 component 的專門機制」
結合上面提到的兩種機制,其實我們已可完成「在子 component 中觸發更新父 component 的 state 資料」的需求,而這些機制也不是為了達成此需求而專門設計的,而是 React 在設計 component 和 state 時就備有這些基本機制,只要組合這些現有的機制,就可達到我們的需求,不需再另外設計專門規則或機制。
當一個 component 的 state 對應的 setState
方法被呼叫時,React 其實不知道、也不在乎:
- 是從哪裡、由誰呼叫這個方法
- 要觸發更新的是哪個 component
- 觸發的 re-render 會不會連帶 re-render 到呼叫
setState
方法的子 component
當一個 setState
方法被呼叫時,React 就只是一視同仁的執行:「一個定義在某 component 中的 state 對應的 setState
方法,根據傳入的參數更新對應的 state 值,並觸發該 state 對應的 component re-render」。
因此,若要更新一個 component 的 state 資料,要怎麼做? 只要在需要觸發更新的地方取得該 state 的 setState
方法,呼叫此方法並傳入新值,就能正確更新該 state 資料並觸發 re-render,這機制無關於:
- 子 component 觸發向上溝通的情境
- 傳遞或取得
setState
方法的手段(不論是以下哪種手段,都不影響setState
的執行)
- 透過 props 傳遞setState
給子 component
- 透過 context 傳遞setState
給任意孫代 component
- 傳遞setState
給 React 應用的外部環境(e.g. Redux)
在子 component 中更新父 component 的 state 資料
回到文章開頭,所以我們要如何在子 component 中更新父 component 的 state 資料? 可由以下兩步驟達成:
- 將父 component 中的 state 對應的
setState
方法直接(或加入其他邏輯封裝成自定義函式)透過 props 傳給子 component - 在子 component 中從 props 取得該
setState
方法(或包含setState
方法的自定義函式),在希望觸發資料更新的地方呼叫它
以以下範例說明整個流程,程式碼如下。
- App.jsx
import { useState } from "react";
import Counter from "./Counter";
export default function App() {
//count state 定義在父 component App,透過 props 傳遞 count state 值和 setCount 方法給 Counter
const [count, setCount] = useState(0);
const isCountOdd = count % 2 === 1;
return (
<div>
<p>Counter 值是 {isCountOdd ? "奇數" : "偶數"}</p>
<Counter count={count} setCount={setCount} />
</div>
);
}
- Counter.jsx
export default function Counter({ count, setCount }) {
//在子 component Counter 的事件處理函式中呼叫由 props 傳遞的 setCount 方法
const decrement = () => setCount(count - 1);
const increment = () => setCount(count + 1);
return (
<div>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
);
}
流程如下:
- App component 首次 render時,將
count
當前狀態值(首次 render 0)和setCount
方法,以 props 形式傳給 Counter component,並連帶 render Counter - 在 Counter 的事件處理中呼叫 props 拿到的
setCount
方法 setCount
方法對應定義在 App component 中的count
state 值,因此會更新count
的值並觸發 App 的 re-render- App re-render,從
useState
回傳值拿到新的 state 值,將新的count
state 值和setCount
方法透過 props 傳給子 component Counter,並連帶 re-render Counter - Counter 被父 component 連帶觸發 re-render,以新的
count
props 資料來產生新的畫面結果
在 Counter 中呼叫 setCount
,為什麼會連帶導致 Counter 本身以新版本 props 被 re-render? 是因為以下幾個 React 現有機制連動導致,並非為了「在子 component 裡觸發更新父 component 的資料」情境而設計:
- 函式可透過 props 傳遞:可將
setState
方法以 props 傳遞給子 component - 呼叫
setState
方法會更新的 state 值和觸發 re-render 的 component 是固定的:傳給子 component 的setState
方法,若在子 component 內被呼叫,仍會觸發定義該 state 的父 component 的 re-render - 父 component re-render 時會連帶觸發子 component 的 re-render:在子 component 呼叫父 component 的
setState
方法時,會觸發父 component 的 re-render,連帶觸發子 component 以新版本的 props 被 re-render
補充:React 的 state 核心機制在維持單向資料流的同時,也能滿足「在子 component 裡觸發更新父 component 的資料」的需求:
- state 需依賴於 component,要綁定在固定的 component 上
- state 只能以對應的setState
方法來更新資料,setState
方法綁定觸發畫面更新的機制(reconciliation)
-setState
方法可以在任何地方維持執行效果
以上機制讓我們能隨意傳遞setState
方法並在任何地方都能更新 state 並觸發 component re-render。呼叫setState
對 React 來說,只是「接收到不知道從哪來更新資料請求」;對該 state 對應的 component 來說,只是「憑空收到新資料並 re-render」,再往下傳遞 props 給子 component,一切運作仍維持由上往下的單向資料流。
batch update
如果我們呼叫 setState
後又立即呼叫 setState
會發生什麼事? 接下來就來看看setState
的運作機制和特性吧~
我們知道呼叫 setState
方法會觸發 component 的 re-render,但 re-render 其實不會被即時觸發,也就是說,呼叫 setState(newValue)
並執行完畢,要執行到下一行程式時, re-render 還沒真正開始。
以以下範例為例,在同個事件處理多次呼叫 setState
方法,實際上最後 re-render 只會發生一次:
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
const handleButtonClick = () => {
setCount(1);
//執行到這裡時,re-render 的動作還不會開始
setCount(2);
//執行到這裡時,re-render 的動作還不會開始
setCount(3);
//✅ 執行到這裡時,此事件沒有其他程式需要執行了,開始進行一次 re-render
};
//...
}
React 會在正在執行的事件內的所有程式結束後,才開始進行 re-render,每次呼叫 setState
時,React 會將呼叫的動作依序記錄到一個待執行計算的佇列(queue)中,最後合併試算只進行一次 re-render 來完成畫面更新。以上面範例來說,會在 handleButtonClick
內的所有 setState
都呼叫完以後,才會執行 component 的 re-render。
延續上方範例,多次呼叫 setState
時,會先將更新值的動作加到待執行佇列中,最後 count 的 state 值會從 0
只經過一次 re-render 直接更新為 3
:
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
const handleButtonClick = () => {
setCount(1); //第一次呼叫時,將"把舊值取代為1"這動作加到待執行佇列中,但還沒 re-render 也還沒實際更新 state 值
setCount(2); //第二次呼叫時,將"把舊值取代為2"這動作加到待執行佇列中,但還沒 re-render 也還沒實際更新 state 值
setCount(3); //第三次呼叫時,將"把舊值取代為3"這動作加到待執行佇列中,但還沒 re-render 也還沒實際更新 state 值
//執行到這時,此事件處理沒有其他後續事情要處理了
//此時會統一進行一次 re-render
//依序試算 count state 的待執行佇列結果:
//原值 -> 把舊值取代為1 -> 把舊值取代為2 -> 把舊值取代為3
//最後將 count state 的值直接更新為 3
};
//...
}
「一個事件中多次呼叫 setState 時,會自動依序合併試算 state 的目標更新結果,最後統一呼叫一次 re-render 來完成畫面更新」,這件事是 React 將多次 re-render 合併為單次以節省效能的機制,此機制稱為「batch update」或「automatic batching」,全由 React 自動判斷並生效,開發者不需自行處理。
batch update 特性:防止以半成品的資料狀態進行 render
batch update 機制可防止 component render 出「半成品」資料狀態對應的錯誤畫面。
import { useState } from "react";
export default function Character(){
const [positionX,setPositionX] = useState(0);
const [positionY,setPositionY] = useState(0);
const moveToButtonRight = () => {
//如果沒有 batch update,setPositionX(positionX + 1)後就會先執行一次 re-render,但 positionY 的資料還沒更新,就會有一瞬間 render 出資料尚未完成更新對應的錯誤畫面
setPositionX(positionX + 1);
setPositionY(positionY + 1);
}
//...
}
在 moveToButtonRight
事件處理中,x 座標與 y 座標的更新在商業邏輯上必須被綁在一起、被視為不可分割的操作,而如果沒有 batch update 機制的話,執行 setPositionX(positionX + 1)
後就會 re-render component,但這時 state 的待執行佇列還沒有 positionY 的更新,此時的 render 會出現 x 座標有更新但 y 座標沒更新的畫面,直到執行 setPositionY(positionY + 1)
後觸發第二次 re-render,才會出現 x 座標和 y 座標都有更新的畫面,這會造成使用者看到一閃而過的錯誤畫面,影響了使用者體驗。
batch update 特性:即使不同 state 的 setState
交叉連續呼叫也支援 batch update
任何 state 對應的 setState
方法混著呼叫時都可支援 batch update。
以下範例可看到,在事件處理中混著呼叫 count和 name 的 setState
方法,最後只會觸發一次 re-render:
import { useState } from "react";
export default function MyComponent(){
const [count, setCount] = useState(0);
const [name, setName] = useState('Chair');
const handleClick = () =>{
//多次混用的 setState 呼叫,只會觸發一次 re-render: 以 3 作為 count 的最新更新結果,以 'Bar' 作為 name 的最新更新結果
setCount(1);
setName('Foo');
setName('Bar');
setCount(2);
setCount(3);
}
//...
}
React 18 對於 batch update 的全面支援
在 React 18 前(<=17),只有直接寫在 event handler 函式中的 setState
方法呼叫才支援 batch update,若在非同步事件中(如:promise、setInterval
、setTimeout
的 callback 中)多次呼叫 setState
方法,仍會觸發多次 re-render。程式碼示意如下:
// React 版本 <= 17 時
import { useState } from "react";
export default function Counter(){
const [count, setCount] = useState(0);
const handleButtonClick = () => {
fetchSomething().then(()=>{
setCount(1);
setCount(2);
setCount(3);
//在 React <=17 時,如果沒有在事件處理函式中呼叫 setState 方法,而是在 promise 等非同步處理的 callback 中呼叫,則無法支援 batch update
})
//此時會為上面三次的 setState 分別依序進行 re-render,共執行三次 re-render
//無法自動支援 batch update
};
return (
<button onClick={handleButtonClick}>
click me
</button>
)
}
但從 React 18 開始,React 會對所有情境下的 setState 完整支援自動 batch update,程式碼示意如下:
// React 版本 >= 18 時
import { useState } from "react";
export default function Counter(){
const [count, setCount] = useState(0);
const handleButtonClick = () => {
fetchSomething().then(()=>{
setCount(1);
setCount(2);
setCount(3);
//在 React >= 18 時,即使在 promise 等非同步處理的 callback 裡呼叫,也能自動支援 batch update
})
//此時 React 會以 3 作為 setCount 的更新結果,只進行一次 re-render
};
return (
<button onClick={handleButtonClick}>
click me
</button>
)
}
因此在 React 18 後,開發者可放心的在任何地方連續呼叫 setState
方法,React 都可自動支援 batch update。
如果我不想要 batch update 時怎麼辦:flushSync()
在多數情況下,自動 batch update 機制對 React 應用是安全且多數是符合預期的,但在某些情況下,我們可能會想在呼叫一次 setState
後,立刻觸發 re-render 並觀察 re-render 造成的畫面結果,此時就可使用 flushSync()
,將 setState
的呼叫放到 flushSync()
的 callback 函式中,React 會立刻呼叫 flushSync()
的 callback 函式,同步更新裡面的任何更新;而如果在 flushSync()
的 callback 內多次呼叫 setState
,一樣也是觸發一次的立即 re-render。
不過官方文件也有提到,flushSync
是不常見的行為,大多數時候不會用到,應將其視為最後的手段。
範例程式碼如下:
// React 版本 >= 18 時
import { useState } from "react";
import { flushSync } from "react-dom";//是從 react-dom import,不是 react
export default function MyComponent(){
const [count, setCount] = useState(0);
const [name, setName] = useState('Chair')
const handleClick = () => {
flushSync(()=>{
setCount(1);
setName('Tea');
//此時會因為這個 flushSync 裡呼叫的 setState 進行一次 re-render
})
// 執行到這裡時,React 已經執行完上面的 setCount(1) 和 setName('Tea')
// 所觸發的 state 更新及 re-render,以及實際的 DOM 已經被操作更新完畢了
flushSync(()=>{
setName('Cake');
//此時會因為這個 flushSync 裡呼叫的 setState 進行一次 re-render
})
// 執行到這裡時,React 已經執行完上面的 setName('Cake');
// 所觸發的 state 更新及 re-render,以及實際的 DOM 已經被操作更新完畢了
}
return (
<button onClick={handleClick}>
click me
</button>
)
}
需注意的是,即使 flushSync()
執行完畢也完成 re-render,若此時接著在事件處理中取得 state 值,會得到還沒被更新的原值,因為正在執行的 handleClick
事件是基於 state 還沒被更新的那次 render 所建立的,而同一次 render 中,state 的值永遠不變,只有在新的一次 render 才能取得新版本的 state 值,程式碼示意如下。
// React 版本 >= 18 時
import { useState } from "react";
import { flushSync } from "react-dom";
export default function MyComponent(){
const [count, setCount] = useState(0);
const handleClick = () => {
flushSync(()=>{
setCount(1);
})
// 執行到這裡時,React 已經執行完上面的 setCount(1)
// 所觸發的 state 更新及 re-render,以及實際的 DOM 已經被操作更新完畢了
//在初始畫面點擊按鈕觸發 handleClick 時,此時讀取到的 count 值為 0,因為在執行的 handleClick 事件是基於 count 為 0 的那次 render 所建立的
//-> 事件處理函式中,裡面會用到的變數值在該次 render 就決定了
console.log('count',count); //0
}
return (
<button onClick={handleClick}>
click me
</button>
)
}
Updater function
如果想基於 state 原有值去逐步計算新值,並連續呼叫 setState
方法,該怎麼做?第一個想法可能是連續呼叫五次 setCount(count + 1)
,並期待最後 count 值變成 5,像這樣的程式碼:
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
const handleButtonClick = () => {
//以目前原有的 state 值做遞增累加,希望點擊按鈕後就會連續累加 +1 五次
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
setCount(count + 1);
};
return (
<>
<h1>{count}</h1>
<button onClick={handleButtonClick}>+5</button>
</>
);
}
但實際執行時,點擊按鈕後 count 只會 +1,更新畫面後結果顯示為 1:
🤯 為什麼畫面結果不如預期?因為每一次 render component 時,都會有該次 render 自己版本的 state 值,且同一次 render 中的 state 值是固定、永遠不變的。
在首次 render 時,我們可將 count 想像為一個值為 0
的常數,在建立 handleButtonClick
函式時,因為 JavaScript 閉包特性,count 值永遠都會是 0
。
在觸發 handleButtonClick
函式時,會執行五次 setCount(count + 1)
,但這五次的 count 值都是 0
,其實等同於呼叫五次的setCount(0 + 1)
,若以 count state 的待執行佇列來看,其結果為:原值 → 把舊值取代為1 → 把舊值取代為1 → 把舊值取代為1 → 把舊值取代為1 → 把舊值取代為1。根據運算結果,最後會把 count 的 state 值更新為 1。
那要如何解決這問題,讓我在呼叫五次 setCount
後,count 值更新為 5? 我們要改用 updater function 的形式來呼叫 setState
方法。
補充:如果對「因為 JavaScript 閉包特性,
handleButtonClick
函式作用域中的 count 變數值永遠都是0
」這種行為感到困惑的話,可能是對 JavaScript 的核心特性「closure(閉包函式)」還不夠熟悉,建議先徹底理解閉包的概念再繼續學習 React。因為 React 的概念與運作大量應用了此 JavaScript 特性。
相關資源可參考:[JS] 深入淺出 JavaScript 閉包(closure)
以 updater function 來進行 setState
方法的呼叫
上篇文章有提到,setState
參數可以是任何型別的值,我們可以把參數分為兩類:
- 傳入目標的新值,如:
setState(1)
- 傳入 updater function,如:
setState(prevValue => prevValue + 1)
其中,傳入目標的新值就是我們前面的用法 setCount(1)
,而傳入 updater function 則會傳入一個函式,此函式在執行時會以舊有的 state 值作為參數(上述 updater function 的 prevValue
參數就會填入舊有 state 的值),且必須回傳要更新的新值。
updater function 的用意在於「以目前為止的原資料經過某些計算後產生新的資料」,其中所謂「某些計算」,就是可以寫在 updater function 內的運算邏輯:
setState((prevValue)=>{
//prevValue 參數會拿到目前為止的 state 值
//...這裡可以加入你想要的運算
const newValue = prevValue * 2;
//函式最後要回傳新的值
return newValue
})
以 updater function 呼叫setState
方法時,一樣有 batch update 機制,updater function 會被加在 state 的待執行佇列中,範例程式碼與示意圖如下。
import { useState } from "react";
export default function Counter(){
const [count, setCount] = useState(0);
const handleButtonClick = () => {
//第一次呼叫 setCount 時,將 preValue => preValue + 1 這動作加到待執行佇列中
setCount(preValue => preValue + 1);
//第二次呼叫 setCount 時,將 preValue => preValue + 1 這動作加到待執行佇列中
setCount(preValue => preValue + 1);
//第三次呼叫 setCount 時,將 preValue => preValue + 1 這動作加到待執行佇列中
setCount(preValue => preValue + 1);
//第四次呼叫 setCount 時,將 preValue => preValue + 1 這動作加到待執行佇列中
setCount(preValue => preValue + 1);
//第五次呼叫 setCount 時,將 preValue => preValue + 1 這動作加到待執行佇列中
setCount(preValue => preValue + 1);
//執行到這裡時,此事件已經沒有其他後續事情要處理
//開始統一執行一次 re-render
//依據 count state 的待執行佇列,依序試算並得到結果
//原值 -> 「preValue => preValue + 1」 -> 「preValue => preValue + 1」 -> 「preValue => preValue + 1」 -> 「preValue => preValue + 1」 -> 「preValue => preValue + 1」
}
//...
}
第一個 updater function 計算時,會將目前 render 的 state 值作為參數 preValue
傳入,此 updater function 回傳值又會做為佇列中下一個 updater function 的參數,示意圖如下。
使用 updater function 的注意事項
- updater function 必須是一個「純函式」,不該包含任何副作用,以保證 updater function 每次執行的結果一致,不會因多次執行而有不同結果、或有預期外的影響
- 嚴格模式中,React 會在執行 updater function 時自動重複跑兩次,以檢查可能包含的副作用(但會採用第一次的執行結果)
補充:副作用與純函式
- 副作用(side effect):指一個函式在執行過程中,對函式的外部環境有任何互動或修改。可能會隨執行次數不同而產生不同效果,並讓函式行為與可能造成的影響變得較難預測。在 JavaScript 的副作用行為如:修改瀏覽器的 DOM 結構、發起 API 請求。
- 純函式(pure function):指一個函式不包含任何副作用,給定相同輸入時,永遠會回傳相同輸出;多次執行都是一樣的執行結果。這讓函式較易推理和測試。
實際應用中,副作用很多時候不可避免,如:讀寫資料庫、網路通訊請求,重要的是了解哪些操作會產生副作用,知道如何管理以確保程式可靠性。
以取代值與 updater function 的動作交叉呼叫 setState
以一個範例說明,交叉以普通取代值和 updater function 來呼叫setState
的過程。
import { useState } from "react";
export default function Counter(){
const [count, setCount] = useState(0);
const handleButtonClick = () => {
//第一次呼叫 setCount 時,將「取代為 count + 3」這動作加到待執行佇列中
setCount(count + 3);
//第二次呼叫 setCount 時,將「preValue => preValue + 6」這動作加到待執行佇列中
setCount(preValue => preValue + 6);
//執行到這裡時,此事件已經沒有其他後續事情要處理
//開始統一執行一次 re-render
//依據 count state 的待執行佇列,依序試算並得到結果
//原值 -> 「取代為 count + 3」 -> 「preValue => preValue + 6」
}
//...
}
在計算 count state 的值時,「取代為某個值」的計算結果也會作為 updater function 的參數,示意如下:
適合用 updater function 的情境案例
回到文章最一開始提到的 Counter 和 CounterControls 的範例,我們在父 component 定義 count state,並由子 component 觸發更新,若我們在子 component 以 updatetr function 來呼叫 setCount
,就不需透過 props 取得父 component 的 count 值,仍能依據既有 state 值延伸計算並更新 state。
因此我們可以把該範例改為如下程式碼:
- Counter.jsx
import { useState } from "react";
import CounterControls from "./CounterControls";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p> Counter value: {count}</p>
{/* 不須傳遞目前 render 的 count 值給 CounterControls*/}
<CounterControls setCount={setCount} />
</div>
);
}
- CounterControls.jsx
export default function CounterControls({ setCount }) {
//以 updatetr function 來呼叫 setCount,就不需透過 props 取得父 component 的 count 值
const decrement = () => setCount(prevCount => prevCount - 1);
const increment = () => setCount(prevCount => prevCount + 1);
const reset = () => setCount(0);
return (
<div>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
<button onClick={reset}>reset</button>
</div>
);
}
Reference:
如有任何問題歡迎聯絡、不吝指教✍️