# [Day 19] 每一次 render 都有自己的 props、state 以及 event handlers + [Day 20] 每一次 render 都有自己的 effects ###### tags: `閱讀筆記` `iT 邦` `一次打破 React 常見的學習門檻與觀念誤解` ## `state` 放在 template 產生的結果只是該次 render 時的結果 `<p>Counter: {counter}</p>` 並不是將 `counter` 丟在 template 後,並且偵測值的變化才會更新 UI,而是透過 `state` 及 `setStateFunc` 告知 React 當這筆資料有變動時就要與歷史 `state` 比對,真的不同才要 re-render 元件。所以 template 上的 `counter` 只是當前元件 re-render 後得出的值(常數)。 ```javascript! const App = () => { const [counter, setCounter] = useState(1) return ( <p>Counter: {counter}</p> ) } ``` ```javascript! const App = () => { const counter = 1 // setStateFunc 回傳的值 return ( <p>Counter: {counter}</p> ) } ``` ```javascript! const App = () => { const counter = 2 // setStateFunc 回傳的值 return ( <p>Counter: {counter}</p> ) } ``` ```javascript! const App = () => { const counter = 3 // setStateFunc 回傳的值 return ( <p>Counter: {counter}</p> ) } ``` ## re-render 是什麼? re-render 就是該元件重新叫用一次,就真的只是當前元件從頭到尾重新叫用一次: ```javascript! const App = () => { const [counter, setCounter] = useState(1) return ( <> <p>Counter: {counter}</p> // re-render 時這些 components 也會重新叫用一次,因為 re-render 就是這個函式的所有東西都重新執行一次 <SomeComponent1 /> <SomeComponent2 /> <SomeComponent3 /> <SomeComponent4 /> </> ) } ``` ### 所以每次 re-render 時都有自己的 `state` 及所有函式 因次,當我們先點擊 `handleToggleAlert` 後,在 `alert` 出現前快速點擊 `handleIncreseCounter` 更動 `counter`,待 `alert` 出現時,會得到叫用 `handleToggleAlert` 時當前的 `counter`: ```javascript! const App = () => { const [counter, setCounter] = useState(1); // 每次 re-render 時都會重新建立一個 handleIncreseCounter,所以每次 re-render handleIncreseCounter 都有自己的 scope const handleIncreseCounter = () => { setCounter((prev) => prev + 1); }; // 每次 re-render 時都會重新建立一個 handleToggleAlert,所以每次 re-render handleToggleAlert 都有自己的 scope const handleToggleAlert = () => { setTimeout(() => { alert(`Counter: ${counter}`); }, 3000); }; return ( <div className="App"> <p>Counter: {counter}</p> <button onClick={handleIncreseCounter}>+ 1</button> <button onClick={handleToggleAlert}>alert!</button> </div> ); } ``` ![](https://hackmd.io/_uploads/SySFszAE3.gif) (會發現 `alert` 會出現 3,而不是 18) 實際上會是這樣子: ```javascript! const App = () => { const counter = 1 // setStateFunc 回傳的值 // 每次 re-render 時都會重新建立一個 handleIncreseCounter,所以每次 re-render handleIncreseCounter 都有自己的 scope const handleIncreseCounter = () => { setCounter((prev) => prev + 1); }; // 每次 re-render 時都會重新建立一個 handleToggleAlert,所以每次 re-render handleToggleAlert 都有自己的 scope const handleToggleAlert = () => { setTimeout(() => { alert(`Counter: ${counter}`); }, 3000); }; return ( // ... ); } ``` ```javascript! const App = () => { const counter = 2 // setStateFunc 回傳的值 // 每次 re-render 時都會重新建立一個 handleIncreseCounter,所以每次 re-render handleIncreseCounter 都有自己的 scope const handleIncreseCounter = () => { setCounter((prev) => prev + 1); }; // 每次 re-render 時都會重新建立一個 handleToggleAlert,所以每次 re-render handleToggleAlert 都有自己的 scope const handleToggleAlert = () => { setTimeout(() => { alert(`Counter: ${counter}`); }, 3000); }; return ( // ... ); } ``` ```javascript! const App = () => { const counter = 3 // setStateFunc 回傳的值 // 每次 re-render 時都會重新建立一個 handleIncreseCounter,所以每次 re-render handleIncreseCounter 都有自己的 scope const handleIncreseCounter = () => { setCounter((prev) => prev + 1); }; // 每次 re-render 時都會重新建立一個 handleToggleAlert,所以每次 re-render handleToggleAlert 都有自己的 scope /* * Step 1: counter = 3 時呼叫了這個函式,因此 setTimeout 內的 callback 會往外找 counter 變數 * Step 2: 瀏覽器開始倒數 * Step 3: 於此同時又一直瘋狂地點擊 handleToggleAlert 更新 state * Step 4: 倒數完 setTimeout 的 callback 被丟入 event queue 排隊 * Step 5: 直到 stack 空了,callback 執行,因為是在 counter = 3 的那次循環叫用,所以 callback 已經取得 counter = 3 的變數,因此就會顯示 3 */ const handleToggleAlert = () => { setTimeout(() => { alert(`Counter: ${counter}`); }, 3000); }; return ( // ... ); } ``` 其他 `handleIncreseCounter` 叫用後的樣子: ```javascript! const App = () => { const counter = 4 // setStateFunc 回傳的值 // 每次 re-render 時都會重新建立一個 handleIncreseCounter,所以每次 re-render handleIncreseCounter 都有自己的 scope const handleIncreseCounter = () => { setCounter((prev) => prev + 1); }; // 每次 re-render 時都會重新建立一個 handleToggleAlert,所以每次 re-render handleToggleAlert 都有自己的 scope const handleToggleAlert = () => { setTimeout(() => { alert(`Counter: ${counter}`); }, 3000); }; return ( // ... ); } // ... 接下來就是這樣子,知道 stack 空了,event queue 的 callback 被推到 stack 後執行該 callback ``` ## 每次 re-render 時也會有自己的 effect function > effect function 就是 `useEffect` 中第一位收的 `callback`。 每次 re-render 就是重新叫用該元件,所以當元件內有 effect function 時,每次 re-render 時也會有自己的 effect function: > 快速複習 useEffect: > - useEffect 收兩個 parameters,第一個為 `callback`,第二個為 `dependency array` > - 不管給不給 dependency array,第一次元件渲染完成後就會叫用一次 `callback` > - useEffect 還可以回傳 callback,稱為 clean up function,當元件 `unmounted` 或者下一次 `callback` 要被叫用前,就會叫用 clean up function > - `dependency array`:[] -> 只會在元件渲染後叫用、不給 -> 每次元件 re-render 都會叫用、[someValues...] -> 讓 React 監聽裡面的值,當裡面的值有更動(與上次 render 時的值不同)就會叫用 effect function ```javascript! export default function App() { const [counter, setCounter] = useState(1); const handleIncreseCounter = () => { setCounter((prev) => prev + 1); }; useEffect(() => { // effect function console.log("after template render", counter); // clean up function return () => { console.log("before next effect function got called", counter); }; // dependency array -> 監聽 counter }, [counter]); return ( // template... ); } ``` 因為有給 `dependency array`,因此在元件第一次渲染後呼叫 effect function 後,React 會監聽 `counter`,當 `counter` 與上次 render 時不同的話就會重新叫用 effect function: ![](https://hackmd.io/_uploads/H1V1ZLCNh.gif) ### 為什麼 clean up function 會拿到上回 render 時的 `state`? 實際來看元件內的流程就會像是這樣: ```javascript! export default function App() { const [counter, setCounter] = useState(1); // Step 2: 透過事件變更 counter const handleIncreseCounter = () => { setCounter((prev) => prev + 1); }; useEffect(() => { // Step 1: 待元件第一次渲染完畢後,叫用內部的 effect function // 會得到 1,因為是在第一次 render 後叫用 effect function // effect function console.log("after temp return () => { console.log("before next effect function got called", counter); }; }, [counter]); return ( // template... ); } ``` ```javascript! export default function App() { // Step 1: setStateFunc 叫用後 React 發現 counter 確實不同,因此觸發 re-render const [counter, setCounter] = useState(1); // -> 此時 counter = 2 const handleIncreseCounter = () => { setCounter((prev) => prev + 1); }; useEffect(() => { // Step 3: 監聽的 counter 確實不同,待上次 render 時的 clean up function 執行完後就執行 effect function // 所以這邊 counter = 2,因為是在 counter = 2 渲染後才叫用 effect function console.log("after template render", counter); // Step 2: 待元件 render 完畢,template 渲染後,先執行上一次 render 時的 clean up function // 所以這邊 counter = 1,因為這個 clean up function 是上次 render 時產生的 // Step 4: 重新建立 counter = 2 時的 clean up function,待下次滿足條件時再次叫用 return () => { console.log("before next effect function got called", counter); }; }, [counter]); return ( // template... ); } ``` 因為每次 render 時元件內的 state、props 及 functions(包含 events、effect functions 及 clean up functions 等...)等都是不變的(都是屬於自己當前的 scope)所以遵從範疇的觀念,clean up function 就是會拿到上回 render 時的資料。 ==因此在處理物件型別的 `state` 時需要特別注意不要使用 mutable way 更新,因為這樣子會破壞每次 render 都是當前 scope 值的概念。== ## 回到文章範例 當我們點選按鈕時會延遲至少 3 秒才會出現 `alert`: ```javascript! const ShowAlert = ({ currentTabName }) => { const handleToggleAlert = () => { setTimeout(() => { alert(`當前選擇的 tab ${currentTabName}`); }, 3000); }; return <button onClick={handleToggleAlert}>Show alert</button>; }; export default ShowAlert; ``` 因為在點擊按鈕觸發 `handleToggleAlert` 時,`handleToggleAlert` 內 callback 的 scope 是屬於當前 render 的,所以自然會保存當前 render 時的 `props`。假設當前是手機遊戲,我們在點擊後切換至電腦遊戲,那麼在 3 秒後還是會出現手機遊戲 -> 是看叫用當下 `props` 是什麼決定 `alert` 出現什麼 -> scope 的原理。 ## 程式範例 [It - React - [Day 19] 每一次 render 都有自己的 props、state 以及 event handlers](https://codesandbox.io/s/it-react-day-19-mei-yi-ci-render-du-you-zi-ji-de-props-state-yi-ji-event-handlers-qdtt8u?file=/src/App.js) [It - React - [Day 20] 每一次 render 都有自己的 effects](https://codesandbox.io/s/it-day-20-mei-yi-ci-render-du-you-zi-ji-de-effects-e667lg?file=/src/App.js) ## Recap - `state` 放在 template 只是單純地渲染當前 render 時 `state` 常數 - 並不是 template 中的 `state` 監聽改動後更新,是 render -> 更新 template - render 就是函式重新叫用一次 - 每次 render 後函式內的東西都是自己的 scope(`state`、`functions`、`props` 等皆是) - 所以每次 render 時的值是不會互相影響的 - 物件型別就要特別注意:使用 immutable update - 維持 React 資料單向流的核心觀念 每次 render 時都是自己的 scope 可以想像成: ```javascript! const render = () => { return { ... } } // 第一次渲染 template = render() // state 更新後重新叫用元件 template = render() // 每次 render 元件都是建立新的 scope... ``` ## 參考資料 1. [[Day 19] 每一次 render 都有自己的 props、state 以及 event handlers](https://ithelp.ithome.com.tw/articles/10304009) 2. [[Day 20] 每一次 render 都有自己的 effects](https://ithelp.ithome.com.tw/articles/10304650)