1.useState
-
使用單個(gè) state 變量還是多個(gè) state 變量
useState 的出現(xiàn)春缕,讓我們可以使用多個(gè) state 變量來保存 state纵竖,比如:
const [left, setLeft] = useState(0); const [top, setTop] = useState(0);
但同時(shí)吗浩,我們也可以像 Class 組件的 this.state 一樣,將所有的 state 放到一個(gè) object 中于个, 這樣只需一個(gè) state 變量即可:
const [state, setState] = useState({ left: 0, top: 0 });
如果使用單個(gè) state 變量锈津,每次更新 state 時(shí)需要合并之前的 state。因?yàn)?useState 返回的 setState 會(huì)替換原來的值隆箩。這一點(diǎn)和 Class 組件的 this.setState 不同该贾。this.setState 會(huì)把更新的字段自動(dòng)合并到 this.state 對象中。
const handleMouseMove = (e) => { setState((prevState) => ({ ...prevState, left: e.pageX, top: e.pageY, })) };
使用多個(gè) state 變量可以讓 state 的粒度更細(xì)摘仅,更易于邏輯的拆分和組合靶庙。比如,我們可以將關(guān)聯(lián)的邏輯提取到自定義 Hook 中:
function usePosition() { const [left, setLeft] = useState(0); const [top, setTop] = useState(0); useEffect(() => { // ... }, []); return [left, top, setLeft, setTop]; }
我們發(fā)現(xiàn)娃属,每次更新 left 時(shí) top 也會(huì)隨之更新六荒。因此,把 top 和 left 拆分為兩個(gè) state 變量顯得有點(diǎn)多余矾端。
在使用 state 之前掏击,我們需要考慮狀態(tài)拆分的「粒度」問題。如果粒度過細(xì)秩铆,代碼就會(huì)變得比較冗余砚亭。如果粒度過粗,代碼的可復(fù)用性就會(huì)降低殴玛。那么捅膘,到底哪些 state 應(yīng)該合并,哪些 state 應(yīng)該拆分呢滚粟?我總結(jié)了下面兩點(diǎn):1.將完全不相關(guān)的 state 拆分為多組 state寻仗。比如 size 和 position。
2.如果某些 state 是相互關(guān)聯(lián)的凡壤,或者需要一起發(fā)生改變署尤,就可以把它們合并為一組 state。 比如 left 和 top亚侠。function Box() { const [position, setPosition] = usePosition(); const [size, setSize] = useState({width: 100, height: 100}); // ... } function usePosition() { const [position, setPosition] = useState({left: 0, top: 0}); useEffect(() => { // ... }, []); return [position, setPosition]; }
-
使用setState更新state的選擇
傳值更新setState(newState);
函數(shù)式更新
如果新的 state 需要通過使用先前的 state 計(jì)算得出曹体,那么可以將函數(shù)傳遞給 setState。該函數(shù)將接收先前的 state硝烂,并返回一個(gè)更新后的值箕别。下面的計(jì)數(shù)器組件示例展示了 setState 的兩種用法:function Counter({initialCount}) { const [count, setCount] = useState(initialCount); return ( <> Count: {count} <button onClick={() => setCount(initialCount)}>Reset</button> <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button> <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button> </> ); }
“+” 和 “-” 按鈕采用函數(shù)式形式,因?yàn)楸桓碌?state 需要基于之前的 state。但是“重置”按鈕則采用普通形式究孕,因?yàn)樗偸前?count 設(shè)置回初始值啥酱。
如果你的更新函數(shù)返回值與當(dāng)前 state 完全相同,則隨后的重渲染會(huì)被完全跳過厨诸。除此之外,我們還可以在其他地方活用函數(shù)式更新禾酱。
有時(shí)候微酬,你的 effect 可能會(huì)使用一些頻繁變化的值。你可能會(huì)忽略依賴列表中 state颤陶,但這通常會(huì)引起 Bug:function Counter() { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => { setCount(count + 1); // 這個(gè) effect 依賴于 `count` state }, 1000); return () => clearInterval(id); }, []); // ?? Bug: `count` 沒有被指定為依賴 return <h1>{count}</h1>; }
傳入空的依賴數(shù)組
[]
颗管,意味著該 hook 只在組件掛載時(shí)運(yùn)行一次,并非重新渲染時(shí)滓走。但如此會(huì)有問題垦江,在setInterval
的回調(diào)中,count
的值不會(huì)發(fā)生變化搅方。因?yàn)楫?dāng) effect 執(zhí)行時(shí)比吭,我們會(huì)創(chuàng)建一個(gè)閉包,并將count
的值被保存在該閉包當(dāng)中姨涡,且初值為0
衩藤。每隔一秒,回調(diào)就會(huì)執(zhí)行setCount(0 + 1)
涛漂,因此赏表,count
永遠(yuǎn)不會(huì)超過 1。
指定[count]
作為依賴列表就能修復(fù)這個(gè) Bug匈仗,但會(huì)導(dǎo)致每次改變發(fā)生時(shí)定時(shí)器都被重置瓢剿。事實(shí)上,每個(gè)setInterval
在被清除前(類似于setTimeout
)都會(huì)調(diào)用一次悠轩。但這并不是我們想要的间狂。要解決這個(gè)問題,我們可以使用setState
的函數(shù)式更新形式哗蜈。它允許我們指定 state 該 如何 改變而不用引用 當(dāng)前 state:function Counter() { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => { setCount(c => c + 1); // ? 在這不依賴于外部的 `count` 變量 }, 1000); return () => clearInterval(id); }, []); // ? 我們的 effect 不適用組件作用域中的任何變量 return <h1>{count}</h1>; }
此時(shí)前标,setInterval 的回調(diào)依舊每秒調(diào)用一次,但每次 setCount 內(nèi)部的回調(diào)取到的 count 最新值(在回調(diào)中變量命名為 c)距潘。
2.useEffect
-
effect清除
通常炼列,組件卸載時(shí)需要清除 effect 創(chuàng)建的諸如訂閱或計(jì)時(shí)器 ID 等資源。要實(shí)現(xiàn)這一點(diǎn)音比,useEffect 函數(shù)需返回一個(gè)清除函數(shù)俭尖。為防止內(nèi)存泄漏,清除函數(shù)會(huì)在組件卸載前執(zhí)行。
- 清除訂閱
useEffect(() => { const subscription = props.source.subscribe(); return () => { // 清除訂閱 subscription.unsubscribe(); }; });
- 避免組件unmount后的state update
useEffect(() => { let ignore = false; async function fetchProduct() { const response = await fetch('http://myapi/product/' + productId); const json = await response.json(); if (!ignore) setProduct(json); } fetchProduct(); return () => { ignore = true }; }, [productId]);
-
使用useEffect deps過多
使用 useEffect hook 時(shí)稽犁,為了避免每次 render 都去執(zhí)行它的 callback焰望,我們通常會(huì)傳入第二個(gè)參數(shù)「dependency array」(下面統(tǒng)稱為依賴數(shù)組)。這樣已亥,只有當(dāng)依賴數(shù)組發(fā)生變化時(shí)熊赖,才會(huì)執(zhí)行 useEffect 的回調(diào)函數(shù)。
function Example({id, name}) { useEffect(() => { // 由于依賴數(shù)組中不包含 name虑椎,所以當(dāng) name 發(fā)生變化時(shí)震鹉,無法打印日志 console.log(id, name); }, [id]); }
在 React 中,除了 useEffect 外捆姜,接收依賴數(shù)組作為參數(shù)的 Hook 還有 useMemo传趾、 useCallback 和 useImperativeHandle。我們剛剛也提到了泥技,依賴數(shù)組中千萬不要遺漏回調(diào)函數(shù)內(nèi)部依賴的值浆兰。但是,如果依賴數(shù)組依賴了過多東西珊豹,可能導(dǎo)致代碼難以維護(hù).
const refresh = useCallback(() => { // ... }, [name, searchState, address, status, personA, personB, progress, page, size]);
不要說內(nèi)部邏輯了簸呈,光是看到這一堆依賴就令人頭大!如果項(xiàng)目中到處都是這樣的代碼平夜,可想而知維護(hù)起來多么痛苦蝶棋。如何才能避免寫出這樣的代碼呢?
首先忽妒,你需要重新思考一下玩裙,這些 deps 是否真的都需要?看下面這個(gè)例子:
function Example({id}) { const requestParams = useRef({}); useEffect(() => { requestParams.current = {page: 1, size: 20, id}; }); const refresh = useCallback(() => { doRefresh(requestParams.current); }, []); useEffect(() => { id && refresh(); }, [id, refresh]); // 思考這里的 deps list 是否合理段直? }
雖然 useEffect 的回調(diào)函數(shù)依賴了 id 和 refresh 方法吃溅,但是觀察 refresh 方法可以發(fā)現(xiàn),它在首次 render 被創(chuàng)建之后鸯檬,永遠(yuǎn)不會(huì)發(fā)生改變了决侈。因此,把它作為 useEffect 的 deps 是多余的喧务。
其次赖歌,如果這些依賴真的都是需要的,那么這些邏輯是否應(yīng)該放到同一個(gè) hook 中功茴?
function Example({id, name, address, status, personA, personB, progress}) { const [page, setPage] = useState(); const [size, setSize] = useState(); const doSearch = useCallback(() => { // ... }, []); const doRefresh = useCallback(() => { // ... }, []); useEffect(() => { id && doSearch({name, address, status, personA, personB, progress}); page && doRefresh({name, page, size}); }, [id, name, address, status, personA, personB, progress, page, size]); }
可以看出庐冯,在 useEffect 中有兩段邏輯,這兩段邏輯是相互獨(dú)立的坎穿,因此我們可以將這兩段邏輯放到不同 useEffect 中:
useEffect(() => { id && doSearch({name, address, status, personA, personB, progress}); }, [id, name, address, status, personA, personB, progress]); useEffect(() => { page && doRefresh({name, page, size}); }, [name, page, size]);
如果邏輯無法繼續(xù)拆分展父,但是依賴數(shù)組還是依賴了過多東西返劲,該怎么辦呢?就比如我們上面的代碼:
useEffect(() => { id && doSearch({name, address, status, personA, personB, progress}); }, [id, name, address, status, personA, personB, progress]);
這段代碼中的 useEffect 依賴了七個(gè)值栖茉,還是偏多了篮绿。仔細(xì)觀察上面的代碼,可以發(fā)現(xiàn)這些值都是「過濾條件」的一部分吕漂,通過這些條件可以過濾頁面上的數(shù)據(jù)亲配。因此,我們可以將它們看做一個(gè)整體痰娱,也就是我們前面講過的合并 state:
const [filters, setFilters] = useState({ name: "", address: "", status: "", personA: "", personB: "", progress: "" }); useEffect(() => { id && doSearch(filters); }, [id, filters]);
如果 state 不能合并弃榨,在 callback 內(nèi)部又使用了 setState 方法,那么可以考慮使用 setState callback 來減少一些依賴梨睁。比如:
const useValues = () => { const [values, setValues] = useState({ data: {}, count: 0 }); const [updateData] = useCallback( (nextData) => { setValues({ data: nextData, count: values.count + 1 // 因?yàn)?callback 內(nèi)部依賴了外部的 values 變量,所以必須在依賴數(shù)組中指定它 }); }, [values], ); return [values, updateData]; };
上面的代碼中娜饵,我們必須在 useCallback 的依賴數(shù)組中指定 values坡贺,否則我們無法在 callback 中獲取到最新的 values 狀態(tài)。但是箱舞,通過 setState 回調(diào)函數(shù)遍坟,我們不用再依賴外部的 values 變量,因此也無需在依賴數(shù)組中指定它晴股。就像下面這樣:
const useValues = () => { const [values, setValues] = useState({}); const [updateData] = useCallback((nextData) => { setValues((prevValues) => ({ data: nextData, count: prevValues.count + 1, // 通過 setState 回調(diào)函數(shù)獲取最新的 values 狀態(tài)愿伴,這時(shí) callback 不再依賴于外部的 values 變量了,因此依賴數(shù)組中不需要指定任何值 })); }, []); // 這個(gè) callback 永遠(yuǎn)不會(huì)重新創(chuàng)建 return [values, updateData]; };
說了這么多电湘,歸根到底都是為了寫出更加清晰隔节、易于維護(hù)的代碼。如果發(fā)現(xiàn)依賴數(shù)組依賴過多寂呛,我們就需要重新審視自己的代碼怎诫。
1.依賴數(shù)組依賴的值最好不要超過 3 個(gè),否則會(huì)導(dǎo)致代碼會(huì)難以維護(hù)贷痪。
2.如果發(fā)現(xiàn)依賴數(shù)組依賴的值過多幻妓,我們應(yīng)該采取一些方法來減少它。
3.去掉不必要的依賴劫拢。
4.將 Hook 拆分為更小的單元肉津,每個(gè) Hook 依賴于各自的依賴數(shù)組。
5.通過合并相關(guān)的 state舱沧,將多個(gè)依賴值聚合為一個(gè)妹沙。
6.通過 setState 回調(diào)函數(shù)獲取最新的 state,以減少外部依賴狗唉。 -
useMemo
該不該使用 useMemo初烘?對于這個(gè)問題,有的人從來沒有思考過,有的人甚至不覺得這是個(gè)問題肾筐。不管什么情況哆料,只要用 useMemo 或者 useCallback 「包裹一下」,似乎就能使應(yīng)用遠(yuǎn)離性能的問題吗铐。但真的是這樣嗎东亦?有的時(shí)候 useMemo 沒有任何作用,甚至還會(huì)影響應(yīng)用的性能唬渗。
為什么這么說呢典阵?首先,我們需要知道 useMemo本身也有開銷镊逝。useMemo 會(huì)「記住」一些值壮啊,同時(shí)在后續(xù) render 時(shí),將依賴數(shù)組中的值取出來和上一次記錄的值進(jìn)行比較撑蒜,如果不相等才會(huì)重新執(zhí)行回調(diào)函數(shù)歹啼,否則直接返回「記住」的值。這個(gè)過程本身就會(huì)消耗一定的內(nèi)存和計(jì)算資源座菠。因此狸眼,過度使用 useMemo 可能會(huì)影響程序的性能。
要想合理使用 useMemo浴滴,我們需要搞清楚 useMemo 適用的場景:
- 有些計(jì)算開銷很大拓萌,我們就需要「記住」它的返回值,避免每次 render 都去重新計(jì)算升略。
- 由于值的引用發(fā)生變化微王,導(dǎo)致下游組件重新渲染,我們也需要「記住」這個(gè)值降宅。
讓我們來看個(gè)例子:
interface IExampleProps { page: number; type: string; } const Example = ({page, type}: IExampleProps) => { const resolvedValue = useMemo(() => { return getResolvedValue(page, type); }, [page, type]); return <ExpensiveComponent resolvedValue={resolvedValue}/>; };
在上面的例子中骂远,渲染 ExpensiveComponent 的開銷很大。所以腰根,當(dāng) resolvedValue 的引用發(fā)生變化時(shí)激才,作者不想重新渲染這個(gè)組件。因此额嘿,作者使用了 useMemo瘸恼,避免每次 render 重新計(jì)算 resolvedValue,導(dǎo)致它的引用發(fā)生改變册养,從而使下游組件 re-render东帅。
這個(gè)擔(dān)憂是正確的,但是使用 useMemo 之前球拦,我們應(yīng)該先思考兩個(gè)問題:
1.傳遞給 useMemo 的函數(shù)開銷大不大靠闭?在上面的例子中帐我,就是考慮 getResolvedValue 函數(shù)的開銷大不大。JS 中大多數(shù)方法都是優(yōu)化過的愧膀,比如 Array.map拦键、Array.forEach 等。如果你執(zhí)行的操作開銷不大檩淋,那么就不需要記住返回值芬为。否則,使用 useMemo 本身的開銷就可能超過重新計(jì)算這個(gè)值的開銷蟀悦。因此媚朦,對于一些簡單的 JS 運(yùn)算來說,我們不需要使用 useMemo 來「記住」它的返回值日戈。
2.當(dāng)輸入相同時(shí)询张,「記憶」值的引用是否會(huì)發(fā)生改變?在上面的例子中浙炼,就是當(dāng) page 和 type 相同時(shí)瑞侮,resolvedValue 的引用是否會(huì)發(fā)生改變?這里我們就需要考慮 resolvedValue 的類型了鼓拧。如果 resolvedValue 是一個(gè)對象,由于我們項(xiàng)目上使用「函數(shù)式編程」越妈,每次函數(shù)調(diào)用都會(huì)產(chǎn)生一個(gè)新的引用季俩。但是,如果 resolvedValue 是一個(gè)原始值(string, boolean, null, undefined, number, symbol)梅掠,也就不存在「引用」的概念了酌住,每次計(jì)算出來的這個(gè)值一定是相等的。也就是說阎抒,ExpensiveComponent 組件不會(huì)被重新渲染酪我。
因此,如果 getResolvedValue 的開銷不大且叁,并且 resolvedValue 返回一個(gè)字符串之類的原始值都哭,那我們完全可以去掉 useMemo,就像下面這樣:
interface IExampleProps { page: number; type: string; } const Example = ({page, type}: IExampleProps) => { const resolvedValue = getResolvedValue(page, type); return <ExpensiveComponent resolvedValue={resolvedValue}/>; };
保持引用不變
// 使用 useMemo function Example() { const users = useMemo(() => [1, 2, 3], []); return <ExpensiveComponent users={users} /> }
在上面的例子中逞带,我們用 useMemo 來「記住」users 數(shù)組欺矫,不是因?yàn)閿?shù)組本身的開銷大,而是因?yàn)?users 的引用在每次 render 時(shí)都會(huì)發(fā)生改變展氓,從而導(dǎo)致子組件 ExpensiveComponent 重新渲染(可能會(huì)帶來較大開銷)穆趴。
在編寫自定義 Hook 時(shí),返回值一定要保持引用的一致性遇汞。因?yàn)槟銦o法確定外部要如何使用它的返回值未妹。如果返回值被用做其他 Hook 的依賴簿废,并且每次 re-render 時(shí)引用不一致(當(dāng)值相等的情況),就可能會(huì)產(chǎn)生 bug络它。比如:
function Example() { const data = useData(); const [dataChanged, setDataChanged] = useState(false); useEffect(() => { setDataChanged((prevDataChanged) => !prevDataChanged); // 當(dāng) data 發(fā)生變化時(shí)族檬,調(diào)用 setState。如果 data 值相同而引用不同酪耕,就可能會(huì)產(chǎn)生非預(yù)期的結(jié)果导梆。 }, [data]); console.log(dataChanged); return <ExpensiveComponent data={data} />; } const useData = () => { // 獲取異步數(shù)據(jù) const resp = getAsyncData([]); // 處理獲取到的異步數(shù)據(jù),這里使用了 Array.map迂烁。因此看尼,即使 data 相同,每次調(diào)用得到的引用也是不同的盟步。 const mapper = (data) => data.map((item) => ({...item, selected: false})); return resp ? mapper(resp) : resp; };
在上面的例子中藏斩,我們通過 useData Hook 獲取了 data。每次 render 時(shí) data 的值沒有發(fā)生變化却盘,但是引用卻不一致狰域。如果把 data 用到 useEffect 的依賴數(shù)組中,就可能產(chǎn)生非預(yù)期的結(jié)果黄橘。另外兆览,由于引用的不同,也會(huì)導(dǎo)致 ExpensiveComponent 組件 re-render塞关,產(chǎn)生性能問題抬探。
因此,在使用 useMemo 之前帆赢,我們不妨先問自己幾個(gè)問題:
1.要記住的函數(shù)開銷很大嗎小压?
2.返回的值是原始值嗎?
3.記憶的值會(huì)被其他 Hook 或者子組件用到嗎椰于?一怠益、應(yīng)該使用 useMemo 的場景
1.保持引用相等
- 對于組件內(nèi)部用到的 object、array瘾婿、函數(shù)等蜻牢,如果用在了其他 Hook 的依賴數(shù)組中,或者作為 props 傳遞給了下游組件憋他,應(yīng)該使用 useMemo孩饼。
- 自定義 Hook 中暴露出來的 object、array竹挡、函數(shù)等镀娶,都應(yīng)該使用 useMemo 。以確保當(dāng)值相同時(shí)揪罕,引用不發(fā)生變化梯码。
- 使用 Context 時(shí)宝泵,如果 Provider 的 value 中定義的值(第一層)發(fā)生了變化,即便用了 Pure Component 或者 React.memo轩娶,仍然會(huì)導(dǎo)致子組件 re-render儿奶。這種情況下,仍然建議使用 useMemo 保持引用的一致性鳄抒。
- 成本很高的計(jì)算
二闯捎、無需使用 useMemo 的場景
- 如果返回的值是原始值: string, boolean, null, undefined, number, symbol(不包括動(dòng)態(tài)聲明的 Symbol),一般不需要使用 useMemo许溅。
- 僅在組件內(nèi)部用到的 object瓤鼻、array、函數(shù)等(沒有作為 props 傳遞給子組件)贤重,且沒有用到其他 Hook 的依賴數(shù)組中茬祷,一般不需要使用 useMemo。
-
useRef
useRef 返回一個(gè)可變的 ref 對象并蝗,其 .current 屬性被初始化為傳入的參數(shù)(initialValue)祭犯。返回的 ref 對象在組件的整個(gè)生命周期內(nèi)保持不變。
1.使用ref訪問子組件function TextInputWithFocusButton() { const inputEl = useRef(null); const onButtonClick = () => { // `current` 指向已掛載到 DOM 上的文本輸入元素 inputEl.current.focus(); }; return ( <> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>Focus the input</button> </> ); }
除此之外滚停,
useRef()
比ref
屬性更有用沃粗。它可以很方便地保存任何可變值,其類似于在 class 中使用實(shí)例字段的方式键畴。
這是因?yàn)樗鼊?chuàng)建的是一個(gè)普通 Javascript 對象陪每。而useRef()
和自建一個(gè){current: ...}
對象的唯一區(qū)別是,useRef
會(huì)在每次渲染時(shí)返回同一個(gè) ref 對象镰吵。- 使用useRef保證引用不變
// 使用 useRef function Example() { const {current: users} = useRef([1, 2, 3]); return <ExpensiveComponent users={users} /> }
- 使用ref保存可變變量
實(shí)現(xiàn)獲取上一輪的 props 或 state
function Counter() { const [count, setCount] = useState(0); const prevCount = usePrevious(count); return <h1>Now: {count}, before: {prevCount}</h1>; } function usePrevious(value) { const ref = useRef(); useEffect(() => { ref.current = value; }); return ref.current; }
實(shí)現(xiàn)effect deps減少
function Example(props) { // 把最新的 props 保存在一個(gè) ref 中 const latestProps = useRef(props); useEffect(() => { latestProps.current = props; }); useEffect(() => { function tick() { // 在任何時(shí)候讀取最新的 props console.log(latestProps.current); } const id = setInterval(tick, 1000); return () => clearInterval(id); }, []); // 這個(gè) effect 從不會(huì)重新執(zhí)行 }