[React] 在子 component 觸發父 component 資料更新、深入理解 batch update 與 updater function

Monica
34 min readApr 12, 2024

--

《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 時就備有這些基本機制,只要組合這些現有的機制,就可達到我們的需求,不需再另外設計專門規則或機制。

組合 React 原有的基礎機制,就可達到需求「在子 component 中觸發更新父 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 資料? 可由以下兩步驟達成:

  1. 將父 component 中的 state 對應的 setState 方法直接(或加入其他邏輯封裝成自定義函式)透過 props 傳給子 component
  2. 在子 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>
);
}

流程如下:

  1. App component 首次 render時,將 count當前狀態值(首次 render 0)和 setCount 方法,以 props 形式傳給 Counter component,並連帶 render Counter
  2. 在 Counter 的事件處理中呼叫 props 拿到的 setCount 方法
  3. setCount 方法對應定義在 App component 中的 count state 值,因此會更新 count 的值並觸發 App 的 re-render
  4. App re-render,從 useState 回傳值拿到新的 state 值,將新的 count state 值和 setCount 方法透過 props 傳給子 component Counter,並連帶 re-render Counter
  5. 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
};
//...
}
batch update 流程示意圖

「一個事件中多次呼叫 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、setIntervalsetTimeout 的 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 呼叫 setState 方法時的 batch update 示意圖

第一個 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:

如有任何問題歡迎聯絡、不吝指教✍️

--

--