每次 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;
逐步操作說明
-
建立元件:
-
在 src 資料夾中創建一個新檔案 Counter.js,並複製上面的程式碼。
-
這個元件接收一個 initialCount 的 prop,並使用 useState 來管理內部的 count 狀態。
-
-
觀察每次渲染的行為:
-
每次你點擊「加 1」按鈕,setCount 會觸發元件重新渲染。
-
打開瀏覽器的開發者工具(按 F12,切到 Console 標籤)。
-
你會看到每次渲染時,console.log 輸出的 initialCount 和 count 是當前渲染的「快照」值。
-
-
為什麼 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;
逐步操作說明
-
建立元件:
- 在 src 資料夾中創建一個新檔案 ButtonCounter.js,並複製上面的程式碼。
-
觀察事件處理函式的行為:
-
每次點擊按鈕,元件會重新渲染。
-
打開瀏覽器 Console,你會看到每次渲染的 handleClick 函式是一個新的記憶體位址(因為它是新創建的函式)。
-
但 handleClick 內的 count 總是正確的,因為它捕獲了當前渲染的 count 值(這是閉包的作用)。
-
-
為什麼每次渲染有新的函式?
-
當元件重新渲染時,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;
逐步操作說明
-
建立元件:
- 在 src 資料夾中創建一個新檔案 Timer.js,並複製上面的程式碼。
-
觀察閉包的行為:
-
這個元件使用 setInterval 每秒更新 seconds。
-
每次點擊「記錄當前秒數」按鈕,logSeconds 會輸出當前渲染的 seconds 值。
-
因為 logSeconds 是每次渲染重新創建的,它總是捕獲最新的 seconds 值,這得益於閉包和不可變的 state。
-
-
為什麼 immutable 資料很棒?
-
如果 state 是可變的(mutable),setInterval 或其他非同步操作可能會引用到錯誤的 seconds 值,導致 bug。
-
因為 state 是不可變的,每次更新 seconds 都會觸發重新渲染,確保 logSeconds 捕獲的是最新的值。
-
閉包讓 logSeconds 能可靠地記住它被創建時的 seconds,而不可變資料保證這些值的正確性。
-
進階說明:閉包陷阱
有時候閉包可能導致意外行為,例如在 useEffect 中使用舊的 state。如果你遇到這種問題,可以使用函式更新形式:
setSeconds((prev) => prev + 1);
這種方式不依賴閉包中的 seconds,而是直接操作前一個狀態值,確保更新正確。
總結與實作建議
-
每次渲染的獨立性:
-
理解 props、state 和事件處理函式在每次渲染中是獨立的,這是 React 核心設計。
-
使用 console.log 來觀察每次渲染的行為,幫助你理解快照的概念。
-
-
性能優化:
-
如果事件處理函式造成不必要的重新渲染,可以使用 useCallback 快取函式。
-
對於複雜的元件,考慮使用 React.memo 避免不必要的子元件渲染。
-
-
不可變資料與閉包:
-
不可變資料讓閉包行為更可靠,減少 bug。
-
當使用非同步操作(如 setInterval 或 setTimeout)時,注意閉包可能捕獲舊值,使用函式更新形式來避免問題。
-