Skip to main content

每次 render 都有自己的 props、state 與 event handler 函式

每次 render 都有其自己版本的 props 與 state

概念解釋

在 React 中,每次元件重新渲染(render)時,React 會創建一個新的「快照」(snapshot)來記錄當前的 props 和 state。這意味著在某次渲染中,你拿到的 props 和 state 是該次渲染的獨立版本,不會被後續的渲染影響。這種設計讓 React 的行為更可預測,因為你不用擔心 props 或 state 在渲染過程中被意外修改。

這就像每次渲染時,React 會幫你「鎖定」一份 props 和 state 的副本,確保你在該次渲染中使用的值是固定的。

程式碼範例

以下是一個簡單的 React 元件,展示每次渲染的 props 和 state 是獨立的。

import React, { useState } from "react";

function Counter({ initialCount }) {
// 使用 useState 定義 state
const [count, setCount] = useState(initialCount);

// 每次點擊按鈕,更新 count
const handleClick = () => {
setCount(count + 1);
};

// 記錄當前渲染的 props 和 state
console.log("當前渲染的 props.initialCount:", initialCount);
console.log("當前渲染的 state.count:", count);

return (
<div>
<p>計數: {count}</p>
<button onClick={handleClick}>1</button>
</div>
);
}

// 使用元件的父元件
function App() {
return (
<div>
<Counter initialCount={0} />
</div>
);
}

export default App;

逐步操作說明

  1. 建立元件

    • 在 src 資料夾中創建一個新檔案 Counter.js,並複製上面的程式碼。

    • 這個元件接收一個 initialCount 的 prop,並使用 useState 來管理內部的 count 狀態。

  2. 觀察每次渲染的行為

    • 每次你點擊「加 1」按鈕,setCount 會觸發元件重新渲染。

    • 打開瀏覽器的開發者工具(按 F12,切到 Console 標籤)。

    • 你會看到每次渲染時,console.log 輸出的 initialCount 和 count 是當前渲染的「快照」值。

  3. 為什麼 props 和 state 是獨立的?

    • 在某次渲染中,initialCount(來自 props)和 count(來自 state)是固定的。

    • 即使 setCount 更新了 count,這次渲染的 count 不會改變,只有下次渲染會拿到新的 count 值。

    • 同樣,initialCount 是父元件傳進來的,父元件改變 initialCount 時,子元件會在下次渲染拿到新的值。

要點總結

  • props 和 state 在每次渲染中是不可變的(immutable),這保證了元件的穩定性。

  • 如果你需要在不同渲染間追蹤值的變化,可以使用 useEffect 或其他 React Hook。


每次 render 都有其自己版本的 event handler 函式

概念解釋

在 React 中,每次元件渲染時,不僅 props 和 state 有自己的版本,連事件處理函式(event handler)也會是一個新的函式實例。這是因為每次渲染會重新執行元件的函式主體,導致所有在元件內定義的函式(包括事件處理函式)都會被重新創建。

雖然每次渲染的函式是新的,但 React 的設計確保這些函式在當前渲染中能正確引用當前的 props 和 state,這得益於 JavaScript 的閉包(closure)機制。

程式碼範例

以下是一個展示事件處理函式獨立性的範例:

import React, { useState } from "react";

function ButtonCounter() {
const [count, setCount] = useState(0);

// 定義事件處理函式
const handleClick = () => {
console.log("當前 count:", count);
setCount(count + 1);
};

// 每次渲染都會輸出 handleClick 的記憶體位址
console.log("handleClick 函式:", handleClick);

return (
<div>
<p>計數: {count}</p>
<button onClick={handleClick}>點我加 1</button>
</div>
);
}

export default ButtonCounter;

逐步操作說明

  1. 建立元件

    • 在 src 資料夾中創建一個新檔案 ButtonCounter.js,並複製上面的程式碼。
  2. 觀察事件處理函式的行為

    • 每次點擊按鈕,元件會重新渲染。

    • 打開瀏覽器 Console,你會看到每次渲染的 handleClick 函式是一個新的記憶體位址(因為它是新創建的函式)。

    • 但 handleClick 內的 count 總是正確的,因為它捕獲了當前渲染的 count 值(這是閉包的作用)。

  3. 為什麼每次渲染有新的函式?

    • 當元件重新渲染時,React 重新執行整個元件函式,handleClick 會被重新定義。

    • 雖然 handleClick 是新的,但它透過閉包記住了當前渲染的 count,所以行為是正確的。

注意事項

  • 如果你需要確保事件處理函式不被重新創建(例如為了性能優化),可以使用 useCallback Hook:
import React, { useState, useCallback } from "react";

function ButtonCounter() {
const [count, setCount] = useState(0);

// 使用 useCallback 快取事件處理函式
const handleClick = useCallback(() => {
console.log("當前 count:", count);
setCount(count + 1);
}, [count]); // 當 count 改變時,重新創建 handleClick

console.log("handleClick 函式:", handleClick);

return (
<div>
<p>計數: {count}</p>
<button onClick={handleClick}>點我加 1</button>
</div>
);
}

export default ButtonCounter;
  • 在這個版本中,useCallback 會快取 handleClick,除非 count 改變,否則不會創建新的函式。這對於傳遞事件處理函式給子元件時特別有用,能避免不必要的重新渲染。

Immutable 資料使得 closure 函式變得可靠而美好

概念解釋

React 的 props 和 state 是不可變的(immutable),這意味著你不能直接修改它們,只能透過 setState 或父元件傳遞新的 props 來更新。這與 JavaScript 的閉包(closure)機制結合,讓事件處理函式在每次渲染中都能可靠地引用正確的 props 和 state。

閉包是指函式能夠記住它被創建時的環境(例如變數)。在 React 中,事件處理函式透過閉包捕獲當前渲染的 props 和 state,即使後續渲染改變了這些值,該函式依然使用它被創建時的「快照」值,這讓程式行為更穩定且可預測。

程式碼範例

以下是一個展示閉包與不可變資料的範例:

import React, { useState, useEffect } from "react";

function Timer() {
const [seconds, setSeconds] = useState(0);

// 使用 setInterval 模擬計時器
useEffect(() => {
const intervalId = setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);

// 清除計時器
return () => clearInterval(intervalId);
}, []); // 空的依賴陣列,確保只在初次渲染時執行

// 事件處理函式,捕獲當前 seconds
const logSeconds = () => {
console.log("當前秒數:", seconds);
};

return (
<div>
<p>經過秒數: {seconds}</p>
<button onClick={logSeconds}>記錄當前秒數</button>
</div>
);
}

export default Timer;

逐步操作說明

  1. 建立元件

    • 在 src 資料夾中創建一個新檔案 Timer.js,並複製上面的程式碼。
  2. 觀察閉包的行為

    • 這個元件使用 setInterval 每秒更新 seconds。

    • 每次點擊「記錄當前秒數」按鈕,logSeconds 會輸出當前渲染的 seconds 值。

    • 因為 logSeconds 是每次渲染重新創建的,它總是捕獲最新的 seconds 值,這得益於閉包和不可變的 state。

  3. 為什麼 immutable 資料很棒?

    • 如果 state 是可變的(mutable),setInterval 或其他非同步操作可能會引用到錯誤的 seconds 值,導致 bug。

    • 因為 state 是不可變的,每次更新 seconds 都會觸發重新渲染,確保 logSeconds 捕獲的是最新的值。

    • 閉包讓 logSeconds 能可靠地記住它被創建時的 seconds,而不可變資料保證這些值的正確性。

進階說明:閉包陷阱

有時候閉包可能導致意外行為,例如在 useEffect 中使用舊的 state。如果你遇到這種問題,可以使用函式更新形式:

setSeconds((prev) => prev + 1);

這種方式不依賴閉包中的 seconds,而是直接操作前一個狀態值,確保更新正確。


總結與實作建議

  1. 每次渲染的獨立性

    • 理解 props、state 和事件處理函式在每次渲染中是獨立的,這是 React 核心設計。

    • 使用 console.log 來觀察每次渲染的行為,幫助你理解快照的概念。

  2. 性能優化

    • 如果事件處理函式造成不必要的重新渲染,可以使用 useCallback 快取函式。

    • 對於複雜的元件,考慮使用 React.memo 避免不必要的子元件渲染。

  3. 不可變資料與閉包

    • 不可變資料讓閉包行為更可靠,減少 bug。

    • 當使用非同步操作(如 setInterval 或 setTimeout)時,注意閉包可能捕獲舊值,使用函式更新形式來避免問題。