# [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>
);
}
```

(會發現 `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:

### 為什麼 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)