# react skill ## useState with callBack Function vs initialValue ```typescript const [count, setCount] = useState(0) const [count2, setCount2] = useState(0) return ( <> <button onClick={() => { // 每次只會加 1 ,因為 react 會根據 count 的值去加 setCount(count + 1) setCount(count + 1) setCount(count + 1) }} >add count</button> <button onClick={() => { // 每次加 3 setCount2(pre => pre + 1) setCount2(pre => pre + 1) setCount2(pre => pre + 1) }} >add count2</button> <div>count2 {count2}</div> <div>count {count}</div> </> ) ``` 因為 `count` 永遠都是 0 ,所以對 react 來說在 batch 時候只會是 `0 + 1` 的結果,所以 `count` 只會加一 1 兒不是加三。 ```typescript setCount(count + 1) setCount(count + 1) setCount(count + 1) ``` 但 `callBack` 則是不一樣,會是 `0 + 1` `1 + 1` `2 + 1` ```typescript setCount2(pre => pre + 1) setCount2(pre => pre + 1) setCount2(pre => pre + 1) ``` ## useEffect clean function觸發時間 ```typescript= useEffect(()=>{ return ()=>{ // clean up function } },[]) ``` clean function執行的時機點有二(React 18) 1. state change 元件更新 → DOM 節點改變 → 畫面渲染 → 執行cleanup 函數 → 執行 Effect 2. unmount 元件更新 → DOM 節點改變 → 畫面渲染 → 執行cleanup 函數 備註: react 17 以前 cleanup 函數是在畫面渲染前執行 1. state change 元件更新 → DOM 節點改變 → 執行cleanup 函數 → 畫面渲染 → 執行 Effect 2. unmount 元件更新 → DOM 節點改變 → 執行cleanup 函數 → 畫面渲染 ## 什麼是Suspense ![](https://hackmd.io/_uploads/HJ9sJTWI3.png) * Suspense 搭配React.lazy去做Code Splitting * 當react render 到 profilePage時如果頁面未準備好,react會先暫停這個render profilePage 這時 react.lazy會先 throw Promise-like(Thenable),並render fallback UI,直到這個component準備好時才渲染。 ### stream 架構中 suspense 處理 在 stream 架構中 server 端可以搭配 client 端的 suspense boundaries 去控制 error ,值得注意的是,如果你在 server 端發生 error 其實並不會去終止 server 端的渲染,取而代之的是,會將 suspense 的 fallBack UI 例如 loading spinner 在你的 initional html 中,但這的情況會導致一個問題是這樣的suspense結果會導致 client 端不會去觸發 error boundary的內容,原因是對 client 端來說他接收到的是一個 successfully 的 suspense 結果。 ![](https://hackmd.io/_uploads/B1IB0uqoh.png) 這樣的現象就變成 server 端造成的 error ,client端只會一直渲染 loading UI,而不是 error boundary 的 fallback 結果,為了解決這個問題,react 官方提供一個解法: ```typescript <Suspense fallback={<Loading />}> <Chat /> </Suspense> function Chat() { if (typeof window === 'undefined') { throw Error('Chat should only render on the client.'); } // ... } ``` 這樣你的 suspense 處理就會讓 client端去做,server 端就會跳過。 但這樣你的 `error 在 server side` 跟 `client side` 的 `error log` 會太多 ### Server-side error: ```typescript Error: Chat should only render on the client.", "at Chat (/Users/Chat.tsx:86:19) ``` ### Client-side error: ```typescript Uncaught Error: Chat should only render on the client. at updateDehydratedSuspenseComponent (react-dom.development.js:20662:1) at updateSuspenseComponent (react-dom.development.js:20362:1) at beginWork (react-dom.development.js:21624:1) at beginWork$1 (react-dom.development.js:27426:1) at performUnitOfWork (react-dom.development.js:26557:1) at workLoopSync (react-dom.development.js:26466:1) at renderRootSync (react-dom.development.js:26434:1) at performConcurrentWorkOnRoot (react-dom.development.js:25738:1) at workLoop (scheduler.development.js:266:1) at flushWork (scheduler.development.js:239:1) ``` 所以比較適合的寫法是透過 `useEffect` 的執行點判斷渲染的 `layout` ```typescript const [client, setClient] = useState(false) useEffect(() => { setClient(true) }, []) if (!client) return null return <Chat /> } ``` ## 什麼是Hydration * server side render 會先初始載入inital html,但這時候頁面還沒有辦法互動,react目前無法控制dom 節點,而Hydration 就是將 event listener等等的js綁到client上。 ## 自動批次處理(Automatic Batching) 什麼事 batch ```typescript import { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0); const handleButtonClick = () => { // 第一次呼叫時,將「取代為 1」這個動作加到待執行佇列中, // 但還沒開始 re-render 也還沒真正更新 state 的值 setCount(1); // 第二次呼叫時,將「取代為 2」這個動作加到待執行佇列中, // 但還沒開始 re-render 也還沒真正更新 state 的值 setCount(2); // 第三次呼叫時,將「取代為 3」這個動作加到待執行佇列中, // 但還沒開始 re-render 也還沒真正更新 state 的值 setCount(3); // 執行到這裡時,這個事件 callback 已經沒有後續的事情需要處理了, // 此時就會開始統一進行一次 re-render, // 並且依序試算 count state 的待執行佇列的結果:原值 => 取代為 1 => 取代為 2 => 取代為 3 // 因此最後會將 count state 的值直接更新成 3 }; // ... } ``` 當你執行 handleButtonClick 實際上 react 並不會直接修改 state 而是透過任務駐守方式執行,最後再統一 render 這叫做 automatic batching ![](https://hackmd.io/_uploads/By387KKO2.png) ## React <= 17 React 版本 <= 17 時也是有 batch 的但是是在同步情況,非同步不會。 ```typescript function App() { const [count, setCount] = useState(0); const [flag, setFlag] = useState(false); function handleClick() { fetchSomething().then(() => { // React 17 以及更早的版本不會 batch 這些 setCount(c => c + 1); // 造成一次 re-render setFlag(f => !f); // 造成一次 re-render }); } function handleClick() { setCount(c => c + 1); setFlag(f => !f); // React 只會 re-render 一次 } } ``` 但如果是 setTimeout 會因為 eventLoop 關係造成多次 render 情況。 ```typescript setTimeout( () => { setCount(1); setCount(2); setCount(3); // 此時會為了上面的三次 setState 分別依序進行 re-render,共三次 re-render, // 無法自動支援 batch update }, 1000 ); ``` 甚至如果是 promise function react 在 useEffect 中會有 undefinded 問題 ```typescript export default function App() { const [name, setname] = useState(); const [address, setaddress] = useState(); useEffect(() => { console.log("useEffect run" + name, address); }, [name, address]); const handleCLick = () => { setname("Leanne Graham"); setaddress({ street: "Kulas Light", suite: "Apt. 556", city: "Gwenborough", zipcode: "92998-3874" }); }; const asynchandleCLick = async () => { const data = await fetch( "https://jsonplaceholder.typicode.com/users/1" ).then((res) => res.json()); console.log(data); setname(data.name); setaddress(data.address); }; const reset = () => { setname(); setaddress(); }; return ( <div className="App"> <p> name :{name}</p> <p> address :{JSON.stringify(address, null, 2)}</p> <button onClick={handleCLick}>click</button> <button onClick={asynchandleCLick}>click async</button> <button onClick={reset}>reset</button> </div> ); } ``` 先試試看 `handleCLick` 一切都如預期,有自動 `batch` ![](https://hackmd.io/_uploads/HyfRLICxT.png) 但當我們執行 `asynchandleCLick` 時候 `useEffect` 第一次 console.log 的 `address` 是 `undefinded` 儘管我們 `fetch api` 有資料,然後 `useEffect` 又會在 `console.log` 一次這時 `address` 才會是有資料的,顯然非同步 `update state`會造成多次的 render 情況,這樣就沒有 `auto bactch`,而且也會造成不必要的 `render` 問題 。 ![](https://hackmd.io/_uploads/S1ECEUReT.png) ## 解決方式 ( react 17 ) 可以試著把多個 `state` 整合到一個 `useState` 中。 ```typescript const [userInfo,setUserInfo]=useState({ name:'', address:{} }) ``` 或是在 `react17` 可以用 `unstable_batchedUpdates` 把 `update state` 包進去 `callback function` ˋ中。 ```typescript import { unstable_batchedUpdates } from 'react-dom' const asynchandleCLick = async () => { unstable_batchedUpdates(()=>{ const data = await fetch( "https://jsonplaceholder.typicode.com/users/1" ).then((res) => res.json()); console.log(data); setname(data.name); setaddress(data.address); }) // React 只會 re-render 一次 }; ``` ## REACT 18 但如果是 `18` 一切都沒有問題了 ```typescript function handleClick() { fetchSomething().then(() => { setCount(c => c + 1); setFlag(f => !f); }); // React 只會 re-render 一次 } setTimeout( () => { setCount(1); setCount(2); setCount(3); // 此時 React 會以 3 作為 setCount 的更新結果,只進行一次 re-render }, 1000 ); function App() { const [count, setCount] = useState(0); const [name, setName] = useState('Zet'); const handleClick = () => { setCount(1); setName('Foo'); setName('Bar'); setCount(2); setCount(3); // 以上的多次且混用的 setState 呼叫 總共只會導致觸發一次 re-render: // 以 3 作為 count 的最後更新結果,且同時以 'Bar' 作為 name 的最後更新結果 }; // promise 也會 auto batching const asynchandleCLick = async () => { const data = await fetch( "https://jsonplaceholder.typicode.com/users/1" ).then((res) => res.json()); console.log(data); setname(data.name); setaddress(data.address); }; // ... } ``` react 18解決無論在 timeout 、promise、native event handler 都會被Automatic Batching https://chentsulin.medium.com/react-%E7%9A%84%E6%9C%AA%E4%BE%86-18-%E4%BB%A5%E5%8F%8A%E5%9C%A8%E9%82%A3%E4%B9%8B%E5%BE%8C-d5764e258deb ### 如果你不想 batch update 呢? 可以透過 flushSync 強制執行 react render ```typescript // React 版本 >= 18 import { flushSync } from 'react-dom'; // 注意:是從 react-dom 裡 import,而不是 react function App() { const [count, setCount] = useState(0); const [name, setName] = useState('Zet'); const willRenderThreeTime = () => { flushSync(() => { setCount(1) }) // re render 一次 flushSync(() => { setCount(2) }) // re render 一次 flushSync(() => { setCount(3) }) // 總共re render 三次 } const onlyRenderOnce = () => { setCount(1) setCount(2) setCount(3) setCount(pre => pre + 1) setName(Math.random().toString()) setCount(pre => pre + 1) } // ... } ``` ## 新 Streaming 架構 Server-Side Rendering ![](https://hackmd.io/_uploads/rJP0Z6b8n.png) 以前ssr不支援 Suspense 造成 render hydration都要一步到位。也就是waterfall 現象,導致UIUX體驗不好。\ 但新的Streaming架構,就可以使用Suspense,就不必等待緩慢的component載入好後才能渲染整個頁面,前端也可包含Spinner的html,透過Chunked Transfer 轉換用 inline script 把 spinner 替換掉 Comments 在page中render的位置 ![](https://hackmd.io/_uploads/HykcmpZLn.png) ## Selective Hydration 跟前面提到的 waterfall ,以前Hydration 都要一步到位,在react 18 Selective Hydration 可以結合Suspense, * 因為Comments還在準備inline script中視loading UI * 這時 react 可以先處理NavBar等前面的的component的Hydration部分 * Comments準備好後將inline script中的loading UI 改成Comments,在做一次Hydration Selective Hydration 可以大大提升 UIUX 的效能,大大減少waterfall問題,改善fcp 時間跟ttl的提前互動。 ![](https://hackmd.io/_uploads/HyFJH6bL3.png) ## Different Hydration Strategies Partial Hydration Partial Hydration 就是只 hydrate 需要互動的 components,其他靜態的部分就沒有必要去 hydrate。 Progressive Hydration Progressive Hydration 則是 需要的時候才 hydrate,例如在 onClick, onFocus 或者元件出現在 viewport 之內。 ### Concurrent feature #### Transition 在整個 page 中不同的 component 彼此都有各自 render cost的時間,透過 react Transition 機制可以為這些 render去做排序,讓 high priority 的 component優先處理,減少畫面卡頓的狀況。 ![](https://hackmd.io/_uploads/BJfm3y6F2.png) ![](https://hackmd.io/_uploads/ryFm3kpK2.png) 所有放到 startTransition 的 state update 都是低優先級的 component,同時可以加配 useTransition 提供的 callback 有 spinner效果 ![](https://hackmd.io/_uploads/SJ0gp1TF2.png) ### 範例 #### before 例如以下的 onChange,因為auto batch的原因,所以 render 會等 setList 完成後才會觸發下一次的 render也就是 setFilter,畫面就會導致很卡,這時你可以使用 useTransition 解決這件事情。 ```typescript const [filter,setFilter]=useState() const [list,setList]=useState() let limit = 100000 const handleChange= (e)=>{ setFilter(e.targe.value) const l = [] for(let i=0;i<=limit,i++){ l.push(e.target.value) } setList(l) } ``` #### after 用 startTransition 包起來的 function 代表這個 state change的 priority 是比較低,所以 setList 會被 react 排到下一次的 render,所以 input 在頁面 onChange 變化就不會因為 setList 而 blocking the UI.,但值得注意的是 useTransition 不要亂用,原因是startTransition 是告訴 react 你要多做一次 render 的動作,所以在使用 useTransition 要記得評估多做一次的render是否可以解決 render卡頓問題,否則反而會增加效能負擔。 ```typescript const [isPending, startTransition] = useTransition() const [filter,setFilter]=useState() const [list,setList]=useState() let limit = 100000 const handleChange= (e)=>{ setFilter(e.targe.value) startTransition(()=>{ const l = [] for(let i=0;i<=limit,i++){ l.push(e.target.value) } setList(l) }) } ``` ## stream render 在 `server component` 中可以透過 `Suspense` 的 `fallback` 做 `loading`的顯示。 ```typescript import React from 'react' const waitFor = () => new Promise((r) => setTimeout(r, 2000)) async function SuspenseCompoment() { await waitFor() return ( <div>Test</div> ) } export default SuspenseCompoment import React, { Suspense } from 'react' import SuspenseCompoment from './components/SuspenseCompoment' async function ConversationsIdPage() { return ( <Suspense fallback="loadig..."> {/* @ts-expect-error Server Component */} <SuspenseCompoment /> </Suspense> ) } export default ConversationsIdPage ``` 值得一提的是 `Next13` 中的 `page.tsx` 預設有包一層 `suspense` 然後 `loading` 可以透過添加 `loading.tsx` 顯示。 ![](https://hackmd.io/_uploads/SJiIOaiZa.png) 想分享一下最近面試 senior react 遇到的考題分享給大家~ 1.setStae 是非同步還是同步。 2.React 18 的渲染流程怎麼觸發。 3.useEffect 裏面 callback 跟 clear function 的執行時間。 4.請解釋 suspense。 5.什麼事 batch update。 6.fiber 是什麼 7.什麼事 immuable state。 8.vue 跟 react 的 vdom 差異是什麼 ## Don’t call Hooks inside loops, conditions, or nested functions. 因為 `loops` 、 `conditions` 、`nested function` 都有可能會造成 `react` 前後次的 `hook` 執行順序或數量不一致的問題,導致觸發 `Render more hooks then previous render ` 發生。 ![螢幕截圖 2023-11-22 09.59.55](https://hackmd.io/_uploads/SyEnXkjVT.png) ![螢幕截圖 2023-11-22 09.59.44](https://hackmd.io/_uploads/ryVnm1sN6.png) ![螢幕截圖 2023-11-22 09.59.31](https://hackmd.io/_uploads/ryEh7koE6.png) [來源](https://www.youtube.com/watch?v=o_7wgRHBzeA&t=60s) ## react render 時機點 (不考慮 StrictMode) 1. 第一次渲染 : adeb 2. 點擊 click : adecb 3. unmount : c ```typescript import React, { useEffect, useState } from 'react' export const Counter = () => { const [count, setCount] = useState<number>(0) console.log('a') useEffect(() => { console.log('b') return () => { console.log('c') } }, [count]) console.log('d') return ( <div> {console.log('e')} <button onClick={() => setCount(count + 1)}>count</button> </div> ) } ``` ## 重點 : 1. 初次渲染不執行 `clean function` 2. `state change` `useEffect` 執行順序 `clean function` 然後 `callback function` 3. `unmount` 只會執行 `clean function` 4. 每次 `render` 整個 `component` 都會 `console.log` ## Don't call react function component https://kentcdodds.com/blog/dont-call-a-react-function-component ## hook ```typescript import { useCallback, useEffect, useRef, useState } from 'react' import { useCounter } from './useCounter' interface UseTimerProps { onTime: () => void time: number delay?: number } export const useTimer = ({ time, onTime, delay = 1000 }: UseTimerProps) => { const callBackRef = useRef<() => void>() const timerIdRef = useRef<NodeJS.Timeout>() const { count, increment, reset } = useCounter(0) useEffect(() => { callBackRef.current = onTime }, [onTime]) useEffect(() => { timerIdRef.current = setInterval(() => { if (count === time / 1000) { callBackRef.current() clearInterval(timerIdRef.current) } else { increment() } }, delay) return () => { clearInterval(timerIdRef.current) } }, [count, delay, increment, time]) return { count, reset } } const useCounter = (initionalValue: number) => { const [count, setCount] = useState(initionalValue) const increment = useCallback(() => setCount(pre => pre + 1), []) const decrement = useCallback(() => setCount(pre => pre - 1), []) const reset = useCallback(() => setCount(initionalValue), []) return { count, increment, decrement, reset, } } ``` ## useEffect vs useLayoutEffect | | useEffect| useLayoutEffect| | -------- | -------- | -------- | | 執行時間 | render 後執行 | render 前執行 | | async / sync | async | sync | | 可否用在 ssr 中 | yes | no | ```typescript import { useEffect, useLayoutEffect, useState } from 'react' function App() { const [isAdmin, setIsAdmin] = useState<boolean>(false) const [useId, setuseId] = useState<number>(0) const now = performance.now() while (performance.now() - now < 300) { } // 這邊把 useEffect 替換成 useLayoutEffect 會有不一樣的效果 useEffect(() => { setIsAdmin(useId === 1) }, [useId]) const handleChangeUser = () => { const anotherUserId = 1 setuseId(anotherUserId) } return ( <> <p>useEffect</p> <p>useId: {useId}</p> <p>isAdmin: {isAdmin ? 'true' : 'false'}</p> <button onClick={handleChangeUser}>changeUser</button> </> ) } export default App ``` 因為 `useEffect` 他是 `non-block state update` 所以你會發現儘管 `userId` 改變了,`isAdmin` 並沒有立即渲染對應的結果,原因是 `useEffect` 的 `non-block` 特性不會等所有 `state update` 後才 `render` 完整的 `UI`,再加上 `component` 中的大量運算,會導致 `useEffect update state` 會有時間差 ,這可能會造成 `user` 畫面上的困惑。 ![ezgif.com-video-to-gif (3)](https://hackmd.io/_uploads/B1CzZ9ZU6.gif) 取而代之你可以使用 `useLayoutEffect` ,他會等所有的 `state` 都 `update` 後才會 `render UI`,但你會發現 `UI` 結果會等一段時間後才會出現,但解決了 `state` 同步的需求。 ![ezgif.com-video-to-gif (2)](https://hackmd.io/_uploads/S1AzZcWLp.gif) #### 總結 所以如果你需要同步 `state` 在渲染 `UI` 的話可以使用 `useLayoutEffect` 但切記大多數情況下不太會去用它,可能會有額外的效能問題,如果你的 `component` 很肥大,使用 `useLayoutEffect` 的 `renter` 結果就會更久,所以謹慎使用~ ### dom 操作的行為放到 useLayoutEffect 中 原因很簡單因為 `useLayoutEffect` 會在 `render` 前執行,可以可以確保 `layoutout` 不會跑版或是白畫面問題。 ## SSR 不支援 useLayoutEffect 在 `useLayoutEffect` 因為不會在 `SSR` 執行所以可能會有非預期的 `hydration` 狀況發生,所以勁量避免。 ```typescript Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://fb.me/react-uselayouteffect-ssr for common fixes. ``` 解決方式就是使用 `customerEffect` ```typescript import { useEffect, useLayoutEffect } from 'react'; const customerEffect = typeof window !== 'undefined' ?useLayoutEffect:useEffect ``` ## react 18 useEffect mount 兩次 ```typescript useEffect(() => { const getPost = async () => { try { const data = await fetch('https://jsonplaceholder.typicode.com/todos/1') } catch (e) { console.log(e) } } getPost() return () => { // abortController.abort() } }, []) ``` 在 react 18 中你會發現 `useEffect` 裡頭的 `request` 被執行兩次 ![截圖 2023-12-09 下午3.49.24](https://hackmd.io/_uploads/BJJm25ZLT.png) 原因其實是 `StrictMode` 關係,`useEffect` 會被 `remount` 兩次,官方是說 `StrictMode` 只會在 `dev` 中發生,`prod` 不會影響到,但這樣儘管是在 `dev` 中還是會有不必要的 `request` 問題發生。 ``` ReactDOM.createRoot(document.getElementById('root')!).render( <React.StrictMode> <App /> </React.StrictMode>, ) ``` ### 解決方式 使用 `AbortController` 在 `unmount` 時 `abort` ```typescript useEffect(() => { const abortController = new AbortController() const getPost = async () => { try { const data = await fetch('https://jsonplaceholder.typicode.com/todos/1', { signal: abortController.signal }) } catch (e) { console.log(e) } } getPost() return () => { abortController.abort() } }, []) ``` 如此一來就可以減少 `request` 浪費摟~ ![截圖 2023-12-09 下午3.53.37](https://hackmd.io/_uploads/S1zzT9bLa.png) ## useSyncExternalStore reSubscrible `useSyncExternalStore` 如果需要把 `subscribe function` 放到 `component` 中時記得要包 `useCallBack` 否則每次 `component rerender` 時都會重新 `re subscribe` ,容易造成效能問題。 ```typescript function ChatIndicator() { const isOnline = useSyncExternalStore(subscribe, getSnapshot); // 🚩 Always a different function, so React will resubscribe on every re-render function subscribe() { // ... } // ... } ``` 解決方式就是加上 `useCallBack` ```typescript function ChatIndicator({ userId }) { const isOnline = useSyncExternalStore(subscribe, getSnapshot); // ✅ Same function as long as userId doesn't change const subscribe = useCallback(() => { // ... }, [userId]); // ... } ``` 或是只在 `hook`中使用 `useSyncExternalStore` ```typescript export function useOnlineStatus() { const isOnline = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); return isOnline; } function getSnapshot() { return navigator.onLine; } function getServerSnapshot() { return true; // Always show "Online" for server-generated HTML } function subscribe(callback) { // ... } ``` ## get Element props to component ```typescript import React from 'react' interface CardProps extends React.HTMLAttributes<HTMLElement> { } export const Card = (props: CardProps) => { return ( <div {...props}>AAA</div> ) } ``` ## Adjusting some state when a props changes 有時候我們可能會需要 `rest component` 中的 `state` ,在每次 `render` 的時候,需求會是每次 `update select` `comment` 會需要全部 `reset` 。 ![截圖 2024-01-06 下午2.00.14](https://hackmd.io/_uploads/S14B6w8uT.png) ```typescript function App() { const [userId, setUerId] = useState<string>('') return ( <> <select name="" id="" onChange={e => setUerId(e.target.value)}> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> </select> <Profile userId={userId} /> </> ) } // ./Profile.tsx import React, { useState } from 'react' interface ProfileProps { userId: string } export const Profile = ({ userId }: ProfileProps) => { const [comment, setComment] = useState<string>('') console.log('Profile render') return ( <> <div>userId {userId}</div> <p>comment : {comment}</p> <input type="text" value={comment} onChange={e => setComment(e.target.value)} /> </> ) } ``` 但上面的 `code` 會有問題,儘管我們 `userId change` 觸發 `react render` 但我們的 `comment` 還是保留最後一次的內容沒有 `reset`。 ![ezgif.com-video-to-gif-converter](https://hackmd.io/_uploads/rkMV0vL_a.gif) ![截圖 2024-01-06 下午2.09.55](https://hackmd.io/_uploads/S1GTCvLd6.png) 正常情況下你的第一直覺可能會用 `useEffect` 的 `dependencies` 去監聽 `userId` 的變化然後 `reset comment`。 ```typescript // ./Profile.tsx import React, { useState } from 'react' interface ProfileProps { userId: string } export const Profile = ({ userId }: ProfileProps) => { const [comment, setComment] = useState<string>('') console.log('Profile render') useEffect(() => { setComment('') }, [userId]) return ( <> <div>userId {userId}</div> <p>comment : {comment}</p> <input type="text" value={comment} onChange={e => setComment(e.target.value)} /> </> ) } ``` 雖然現在可以 `reset` ,但其實可能會有淺在的效能問題。 ![ezgif.com-video-to-gif-converter](https://hackmd.io/_uploads/BJXcJdU_6.gif) #### you might not need useEffect 使用 `useEffect` 會重新觸發 `react rerender` ,也就是你的 `dom` 節點將會傳不重新 `calc` 與排序,重新跑一次 `reconciliation` ,這樣帶來的 `side effect` 會是如果你的 `Profile` 節點太肥,會因為 `reset` 關係導致所有 `dom` 都需要重新計算,效能會有問題。 解決方式就是把 `useEffect` 拿掉改用 `state` 如下: 透過 `preState` 的方式,去做 `reset` ,一方面是實現 `useEfect` 中呼叫`callBack` 的方式,同時也不會有效能上 `side Effect` 的問題。 ```typescript interface ProfileProps { userId: string } export const Profile = ({ userId }: ProfileProps) => { const [comment, setComment] = useState<string>('') const [preItems, setPreItems] = useState(userId) if (preItems !== userId) { setPreItems(userId) setComment('') } return ( <> <div>userId {userId}</div> <p>comment : {comment}</p> <input type="text" value={comment} onChange={e => setComment(e.target.value)} /> </> ) } ``` 但這樣可能還不是最佳解如果你的 `props` 更多那是不是也要用更多的 `preState` 去判斷,這時你可以考慮用 `key` 的方式。 ```typescript function App() { const [userId, setUerId] = useState<string>('') return ( <> //... <Profile userId={userId} key={userId} /> </> ) } ``` 同時 `Profile` 也不需要額外再判斷 `preState` `code` 是不是更乾淨了呢~ ```typescript import React, { useEffect, useState } from 'react' interface ProfileProps { userId: string } export const Profile = ({ userId }: ProfileProps) => { const [comment, setComment] = useState<string>('') return ( <> <div>userId {userId}</div> <p>comment : {comment}</p> <input type="text" value={comment} onChange={e => setComment(e.target.value)} /> </> ) } ``` ## useDeferredValue vs debounce 1. `useDeferredValue` 他不需要自己設定` debounce timeout` 會是根據 `user` 的當下環境自己決定 `debounce` 時間,設備好的 `debounce` 更快 1. 這個 `hook` 是直接綁定 `react` 的 `render` 生命週期 1. `useDeferredValue` 他的 `render` 是可以中斷的,而 `debounce` 則不行,什麼意思呢假設 `user` 在 `input` 輸入 3 次 ,` a->ab->abc` ,`denounce` 在 `ui` 上就會依序去 `render` ,但 `useDeferredValue` 只會 `render` 最後一次 `abc` 跟 `batch update` 很像。 [react 官網](https://react.dev/reference/react/useDeferredValue) ## use API * 可以搭配 `context` 跟 `promise` 使用 * `use API` 可以用 `loop` 或是 `conditional statements`