React Hook挖坑
- 2020 年 4 月 5 日
- 筆記
React Hook挖坑
如果已經使用過 Hook,相信你一定回不去了,這種用函數的方式去編寫有狀態組件簡直太爽啦。
如果還沒使用過 Hook,那你要趕緊升級你的 React(v16.8+),投入 Hook 的懷抱吧。
至於 Hook 的好處這裡就不多說了,上一篇已經講過了——React Hook上車。
Hook 雖好,操作不當可是容易翻車的哦。
下面,我們就來聊聊在使用過程中可能遇到的坑吧……
useState
useState 只在組件首次渲染的時候執行
坑:useState的初始值,只在第一次有效
證據:
當點擊按鈕修改name的值的時候,我發現在Child組件,是收到了,但是並沒有通過useState
賦值給name!
const Child = ({data}) =>{ console.log('child render...', data) // 每次更新都會執行 const [name, setName] = useState(data) // 只會在首次渲染組件時執行 return ( <div> <div>child</div> <div>{name} --- {data}</div> </div> ); } const Hook =()=>{ console.log('Hook render...') const [name, setName] = useState('rose') return( <div> <div> {count} </div> <button onClick={()=>setName('jack')}>update name </button> <Child data={name}/> </div> ) }
想在第一次 render 前執行的程式碼放 useState() 裡面
上面我們已經知道了useState()
只會在第一次渲染的時候才執行,那麼這有什麼實用價值嗎?答案:可以把第一次 render 前執行的程式碼放入其中。
例如:
const instance = useRef(null); useState(() => { instance.current = 'initial value'; });
類似 class component 里的constructor
和componentWillMount
。
useState 里數據必須為 immutable
啥?你還不知道 immutable 是個啥?甩手就是兩個鏈接:Immutable.js 了解一下、Immutable 詳解及在 React 實踐。
什麼是 Immutable Data?
首先,你要知道 JavaScript 中的對象一般是可變的(Mutable Data),因為使用了引用賦值。
Immutable Data 就是一旦創建,就不能再被更改的數據。對 Immutable 對象的任何修改或添加刪除操作都會返回一個新的 Immutable 對象。
雖然 class component 的 state 也提倡使用 immutable data,但不是強制的,因為只要調用了setState
就會觸發更新。
但是使用useState
時,如果在更新函數里傳入同一個對象將無法觸發更新。
證據:
const [list, setList] = useState([2,32,1,534,44]); return ( <> <ol> {list.map(v => <li key={v}>{v}</li>)} </ol> <button onClick={() => { // bad:這樣無法觸發更新! setList(list.sort((a, b) => a - b)); // good:必須傳入一個新的對象! setList(list.slice().sort((a, b) => a - b)); }} >sort</button> </> )
useState 過時的閉包
之前就說過,Hook 產生問題時,90%都是閉包引起的。下面就來看一下這個詭異的bug:
function DelayCount() { const [count, setCount] = useState(0); function handleClickAsync() { setTimeout(function delay() { setCount(count + 1); // 問題所在:此時的 count 為1s前的count!!! }, 1000); } function handleClickSync() { setCount(count + 1); } return ( <div> {count} <button onClick={handleClickAsync}>非同步加1</button> <button onClick={handleClickSync}>同步加1</button> </div> ); }
點擊「非同步加1」按鍵,然後立即點擊「同步加1」按鈕。你會驚奇的發現,count 只更新到 1。
這是因為 delay() 是一個過時的閉包。
來看看這個過程發生了什麼:
- 初始渲染:count 值為 0。
- 點擊「非同步加1」按鈕,delay() 閉包捕獲 count 的值 0,setTimeout() 1 秒後調用 delay()。
- 點擊「同步加1」按鈕,handleClickSync() 調用 setCount(0 + 1) 將 count 的值設置為 1,組件重新渲染。
- 1 秒之後,setTimeout() 執行 delay() 。但是 delay() 中閉包保存 count 的值是初始渲染的值 0,所以調用 setState(0 + 1),結果count保持為 1。
delay() 是一個過時的閉包,它使用在初始渲染期間捕獲的過時的 count 變數。
為了解決這個問題,可以使用函數方法來更新 count
狀態:
function DelayCount() { const [count, setCount] = useState(0); function handleClickAsync() { setTimeout(function delay() { setCount(count => count + 1); // 重點:setCount傳入的回調函數用的是最新的 state!!! }, 1000); } function handleClickSync() { setCount(count + 1); } return ( <div> {count} <button onClick={handleClickAsync}>非同步加1</button> <button onClick={handleClickSync}>同步加1</button> </div> ); }
useEffect
如何在 useEffect 中使用 async
上一篇文章中我們提到過:useEffect的 callback 函數要麼返回一個能清除副作用的函數,要麼就不返回任何內容。
而 async 函數返回的是 Promise 對象,那我們要怎麼在 useEffect 的callback 中使用 async 呢?
最簡單的方法是IIFE
(自執行函數):
useEffect(() => { (async () => { await fetchSomething(); })(); }, []);
useEffect 死循環
- useEffect 在傳入第二個參數時一定注意:第二個參數不能為引用類型,會造成死循環。
比如:[]===[] 為false,所以會造成 useEffect 會一直不停的渲染。 - useEffect 的 callback 函數中改變的 state 一定不能在該 useEffect 的依賴數組中。比如:
useEffect(()=>{ setCount(count); }, [count]);
依賴 count,callback 中又 setCount(count)。
推薦啟用 eslint-plugin-react-hooks
中的 exhaustive-deps
規則。此規則會在添加錯誤依賴時發出警告並給出修復建議。
函數作為依賴的時候死循環
有時候,我們需要將函數作為依賴項傳入依賴數組中,例如:
// 子組件 let Child = React.memo((props) => { useEffect(() => { props.onChange(props.id) }, [props.onChange, props.id]); return ( <div>{props.id}</div> ); }); // 父組件 let Parent = () => { let [id, setId] = useState(0); let [count, setCount] = useState(0); const onChange = (id) => { // coding setCount(id); } return ( <div> {count} <Child onChange={onChange} id={id} /> // 重點:這裡有性能問題!!! </div> ); };
程式碼中重點位置,每次父組件render,onChange引用值肯定會變。因此,子組件Child必定會render,子組件觸發useEffect,從而再次觸發父組件render….循環往複,這就會造成死循環。下面我們來優化一下:
// 子組件 let Child = React.memo((props) => { useEffect(() => { props.onChange(props.id) }, [props.onChange, props.id]); return ( <div>{props.id}</div> ); }); // 父組件 let Parent = () => { let [id, setId] = useState(0); let [count, setCount] = useState(0); const onChange = useCallback(() => { // 重點:通過useCallback包裹一層即可達到快取函數的目的 // coding }, [id]); // id 為依賴值 return ( <div> {count} <Child onChange={onChange} id={id} /> // 重點:這個onChange在每次父組件render都會改變! </div> ); };
useCallback
將返回該回調函數的 memoized 版本,該回調函數僅在某個依賴項改變時才會更新。當你把回調函數傳遞給經過優化的並使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate
)的子組件時,它將非常有用。
useCallback(fn, deps)
相當於useMemo(() => fn, deps)
useEffect 裡面拿不到最新的props和state
useEffect 裡面使用到的state的值, 固定在了useEffect內部, 不會被改變,除非useEffect刷新,重新固定state的值
function Example() { const [count, setCount] = useState(0); const latestCount = useRef(count); useEffect(() => { // Set the mutable latest value latestCount.current = count; setTimeout(() => { // Read the mutable latest value console.log(`You clicked ${latestCount.current} times`); }, 3000); });
總結
以上只是收集了一部分工作中可能會遇到的坑,大致分為2種:
- 閉包引起的 state 值過期
- 依賴值監聽問題導致死循環
以後遇到其他的問題會繼續補充…
參考:
react hooks踩坑記錄
使用 JS 及 React Hook 時需要注意過時閉包的坑(文中有解決方法)