如果已經使用過 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 為5s前的count!!!
}, 5000);
}
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() 5秒后調用 delay()。
- 點擊“同步加1”按鈕,handleClickSync() 調用 setCount(0 + 1) 將 count 的值設置為 1,組件重新渲染。
- 5秒之后,setTimeout() 執行 delay() 。但是 delay() 中閉包保存 count 的值是初始渲染的值 0,所以調用 setState(0 + 1),結果count保持為 1。
每次 render 都會產生新的閉包。delay() 是一個過時的閉包,它使用在5s前捕獲的過時的 count 變量。
為了解決這個問題,可以使用函數方法來更新 count
狀態:
function DelayCount() {
const [count, setCount] = useState(0);
function handleClickAsync() {
setTimeout(function delay() {
setCount(count => count + 1); // 重點:setCount傳入的回調函數用的是最新的 state!!!
}, 5000);
}
function handleClickSync() {
setCount(count + 1);
}
return (
<div>
{count}
<button onClick={handleClickAsync}>異步加1</button>
<button onClick={handleClickSync}>同步加1</button>
</div>
);
}
關於 Hook 中的閉包:
useEffect
、useMemo
、useCallback
都是自帶閉包的。也就是說,每一次組件的渲染,其都會捕獲當前組件函數上下文中的狀態(state、props)。所以每一次這三種 Hook 的執行,反映的也都是當時的狀態,無法獲取最新的狀態。對於這種情況,應該使用 ref 來訪問。
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 的值。
useRef
保存任何可變化的值,.current
屬性總是取最新的值。就是相當於全局作用域,一處被修改,其他地方全更新...
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踩坑記錄