你可能不知道的 React Hooks

2021-03-02 前端工匠

本文是譯文,原文地址是:https://medium.com/@sdolidze/the-iceberg-of-react-hooks-af0b588f43fb

React Hooks 與類組件不同,它提供了用於優化和組合應用程式的簡單方式,並且使用了最少的樣板文件。

如果沒有深入的知識,由於微妙的 bug 和抽象層漏洞,可能會出現性能問題,代碼複雜性也會增加。

我已經創建了 12 個案例研究來演示常見的問題以及解決它們的方法。 我還編寫了 React Hooks RadarReact Hooks Checklist,來推薦和快速參考。

案例研究: 實現 Interval

目標是實現計數器,從 0 開始,每 500 毫秒增加一次。 應提供三個控制按鈕: 啟動、停止和清除。

Level 0:Hello World
export default function Level00() {
console.log('renderLevel00');
const [count, setCount] = useState(0);
return (
<div>
count => {count}
<button onClick={() => setCount(count + 1)}>+</button>
<button onClick={() => setCount(count - 1)}>-</button>
</div>
);
}

這是一個簡單的、正確實現的計數器,用戶單擊時計數器的增加或減少。

Level 1:setInterval
export default function Level01() {
console.log('renderLevel01');
const [count, setCount] = useState(0);
setInterval(() => {
setCount(count + 1);
}, 500);
return <div>count => {count}</div>;
}

此代碼的目的是每 500 毫秒增加計數器。 這段代碼存在巨大的內存洩漏並且實現不正確。 它很容易讓瀏覽器標籤崩潰。 由於 Level01 函數在每次渲染發生時被調用,所以每次觸發渲染時這個組件都會創建新的 interval。

突變、訂閱、計時器、日誌記錄和其他副作用不允許出現在函數組件的主體中(稱為 React 的 render 階段)。 這樣做會導致用戶界面中的錯誤和不一致。

Hooks API Reference[1]: useEffect[2]

Level 2:useEffect
export default function Level02() {
console.log('renderLevel02');
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
setCount(count + 1);
}, 500);
});
return <div>Level 2: count => {count}</div>;
}

大多數副作用放在 useEffect 內部。 但是此代碼還有巨大的資源洩漏,並且實現不正確。 useEffect 的默認行為是在每次渲染後運行,所以每次計數更改都會創建新的 Interval

Hooks API Reference[3]: useEffect[4], Timing of Effects[5].

Level 3: 只運行一次
export default function Level03() {
console.log('renderLevel03');
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
setCount(count + 1);
}, 300);
}, []);
return <div>count => {count}</div>;
}

[] 作為 useEffect 的第二個參數,將在 mount 之後只調用一次 function,即使只調用一次 setInterval,這段代碼的實現也是不正確的。

雖然 count 會從 0 增加到 1,但是不會再增加,只會保持成 1。 因為箭頭函數隻被創建一次,所以箭頭函數裡面的 count 會一直為 0.

這段代碼也存在微妙的資源洩漏。 即使在組件卸載之後,仍將調用 setCount。

Hooks API Reference[6]: useEffect[7], Conditionally firing an effect[8].

Level 4:清理
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1);
}, 300);
return () => clearInterval(interval);
}, []);

為了防止資源洩漏,Hooks 的生命周期結束時,必須清理所有內容。 在這種情況下,組件卸載後將調用返回的函數。

這段代碼沒有資源洩漏,但是實現不正確,就像之前的代碼一樣。

Hooks API Reference[9]: Cleaning up an effect[10].

Level 5:使用 count 作為依賴項
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1);
}, 500);
return () => clearInterval(interval);
}, [count]);

useEffect 提供依賴數組會改變它的生命周期。 在這個例子中,useEffectmount 之後會被調用一次,並且每次 count 都會改變。 清理函數將在每次 count 更改時被調用以釋放前面的資源。

這段代碼工作正常,沒有任何錯誤,但是還是有點不好,每 500 毫秒創建和釋放 setInterval, 每個 setInterval 總是調用一次。

Hooks API Reference[11]: useEffect[12], Conditionally firing an effect[13].

Level 6:setTimeout
useEffect(() => {
const timeout = setTimeout(() => {
setCount(count + 1);
}, 500);
return () => clearTimeout(timeout);
}, [count]);

這段代碼和上面的代碼可以正常工作。 因為 useEffect 是在每次 count 更改時調用的,所以使用 setTimeout 與調用 setInterval 具有相同的效果。

這個例子效率很低,每次渲染發生時都會創建新的 setTimeout,React 有一個更好的方式來解決問題。

Level 7:useState 的函數更新
useEffect(() => {
const interval = setInterval(() => {
setCount(c => c + 1);
}, 500);
return () => clearInterval(interval);
}, []);

在前面的例子中,我們對每次 count 更改運行 useEffect,這是必要的,因為我們需要始終保持最新的當前值。

useState 提供 API 來更新以前的狀態,而不用捕獲當前值。 要做到這一點,我們需要做的就是向 setState 提供 lambda(匿名函數)。

這段代碼工作正常,效率更高。 在組件的生命周期中,我們使用單個 setInterval, clearInterval 只會在卸載組件之後調用一次。

Hooks API Reference[14]: useState[15], Functional updates[16].

Level 8:局部變量
export default function Level08() {
console.log('renderLevel08');
const [count, setCount] = useState(0);
let interval = null;

const start = () => {
interval = setInterval(() => {
setCount(c => c + 1);
}, 500);
};
const stop = () => {
clearInterval(interval);
};
return (
<div>
count => {count}
<button onClick={start}>start</button>
<button onClick={stop}>stop</button>
</div>
);
}

我們增加了 start 和 stop 按鈕。 此代碼實現不正確,因為 stop 按鈕不工作。 因為在每次渲染期間都會創建新的引用(指 interval 的引用),因此 stop 函數裡面 clearInterval 裡面的 interval 是 null。

Hooks API Reference[17]: Is there something like instance variables?[18]

Level 9:useRef
export default function Level09() {
console.log('renderLevel09');
const [count, setCount] = useState(0);
const intervalRef = useRef(null);

const start = () => {
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 500);
};

const stop = () => {
clearInterval(intervalRef.current);
};

return (
<div>
count => {count}
<button onClick={start}>start</button>
<button onClick={stop}>stop</button>
</div>
);
}

如果需要變量,useRef 是首選的 Hook。 與局部變量不同,React 確保在每次渲染期間返回相同的引用。

這個代碼看起來是正確的,但是有一個微妙的錯誤。 如果 start 被多次調用,那麼 setInterval 將被多次調用,從而觸發資源洩漏。

Hooks API Reference[19]: useRef[20]

Level 10: 判空處理
export default function Level10() {
console.log('renderLevel10');
const [count, setCount] = useState(0);
const intervalRef = useRef(null);

const start = () => {
if (intervalRef.current !== null) {
return;
}
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 500);
};

const stop = () => {
if (intervalRef.current === null) {
return;
}
clearInterval(intervalRef.current);
intervalRef.current = null;
};

return (
<div>
count => {count}
<button onClick={start}>start</button>
<button onClick={stop}>stop</button>
</div>
);
}

為了避免資源洩漏,如果 interval 已經啟動,我們只需忽略調用。 儘管調用 clearInterval (null) 不會觸發任何錯誤,但是只釋放一次資源仍然是一個很好的實踐。

此代碼沒有資源洩漏,實現正確,但可能存在性能問題。

memoizationReact 中主要的性能優化工具。 React.memo 進行淺比較,如果引用相同,則跳過 render 階段。

如果 start 函數 和 stop 函數被傳遞給一個 memoized 組件,整個優化就會失敗,因為在每次渲染之後都會返回新的引用。

React Hooks: Memoization[21]

Level 11: useCallback
const intervalRef = useRef(null);

const start = useCallback(() => {
if (intervalRef.current !== null) {
return;
}
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 500);
}, []);

const stop = useCallback(() => {
if (intervalRef.current === null) {
return;
}

clearInterval(intervalRef.current);
intervalRef.current = null;
}, []);

return (
<div>
count => {count}
<button onClick={start}>start</button>
<button onClick={stop}>stop</button>
</div>
);
}

為了使 React.memo 能夠正常工作,我們需要做的就是使用 useCallback 來記憶(memoize)函數。 這樣,每次渲染後都會提供相同的函數引用。

此代碼沒有資源洩漏,實現正確,沒有性能問題,但代碼相當複雜,即使對於簡單的計數器也是如此。

Hooks API Reference[22]: useCallback[23]

Level 12: 自定義 Hook
function useCounter(initialValue, ms) {
const [count, setCount] = useState(initialValue);
const intervalRef = useRef(null);

const start = useCallback(() => {
if (intervalRef.current !== null) {
return;
}
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, ms);
}, []);

const stop = useCallback(() => {
if (intervalRef.current === null) {
return;
}
clearInterval(intervalRef.current);
intervalRef.current = null;
}, []);

const reset = useCallback(() => {
setCount(0);
}, []);

return { count, start, stop, reset };
}

為了簡化代碼,我們需要將所有複雜性封裝在 useCounter 自定義鉤子中,並暴露 api: { count,start,stop,reset }。

export default function Level12() {
console.log('renderLevel12');
const { count, start, stop, reset } = useCounter(0, 500);

return (
<div>
count => {count}
<button onClick={start}>start</button>
<button onClick={stop}>stop</button>
<button onClick={reset}>reset</button>
</div>
);
}

Hooks API Reference[24]: Using a Custom Hook[25]

React Hooks Radar✅ Green

綠色 hooks 是現代 React 應用程式的主要構件。 它們幾乎在任何地方都可以安全地使用,而不需要太多的思考

🌕 Yellow

黃色 hooks 通過使用記憶(memoize)提供了有用的性能優化。 管理生命周期和輸入應該謹慎地進行。

🔴 Red

紅色 hooks 與易變的世界相互作用,使用副作用。 它們是最強大的,應該極其謹慎地使用。 自定義 hooks 被推薦用於所有重要用途的情況。

用好 React Hooks 的清單服從Rules of Hooks 鉤子的規則[26].Prefer 更喜歡useReducer or functional updates for 或功能更新useStateto prevent reading and writing same value in a hook. 防止在鉤子上讀寫相同的數值不要在渲染函數中使用可變變量,而應該使用useRef如果你保存在useRef 的值的生命周期小於組件本身,在處理資源時不要忘記取消設置值在需要的時候使用 Memoize 函數和對象來提高性能正確捕獲輸入依賴項(undefined=> 每一次渲染,[a, b] => 當a or 或b改變的時候渲染, 改變,[] => 只改變一次)對於複雜的用例可以通過自定義 Hooks 來實現。參考資料[1]

Hooks API Reference: https://reactjs.org/docs/hooks-reference.html

[2]

useEffect: https://reactjs.org/docs/hooks-reference.html#useeffect

[3]

Hooks API Reference: https://reactjs.org/docs/hooks-reference.html

[4]

useEffect: https://reactjs.org/docs/hooks-reference.html#useeffect

[5]

Timing of Effects: https://reactjs.org/docs/hooks-reference.html#timing-of-effects

[6]

Hooks API Reference: https://reactjs.org/docs/hooks-reference.html

[7]

useEffect: https://reactjs.org/docs/hooks-reference.html#useeffect

[8]

Conditionally firing an effect: https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect

[9]

Hooks API Reference: https://reactjs.org/docs/hooks-reference.html

[10]

Cleaning up an effect: https://reactjs.org/docs/hooks-reference.html#cleaning-up-an-effect

[11]

Hooks API Reference: https://reactjs.org/docs/hooks-reference.html

[12]

useEffect: https://reactjs.org/docs/hooks-reference.html#useeffect

[13]

Conditionally firing an effect: https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect

[14]

Hooks API Reference: https://reactjs.org/docs/hooks-reference.html

[15]

useState: https://reactjs.org/docs/hooks-reference.html#usestate

[16]

Functional updates: https://reactjs.org/docs/hooks-reference.html#functional-updates

[17]

Hooks API Reference: https://reactjs.org/docs/hooks-reference.html

[18]

Is there something like instance variables?: https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables

[19]

Hooks API Reference: https://reactjs.org/docs/hooks-reference.html

[20]

useRef: https://reactjs.org/docs/hooks-reference.html#useref

[21]

React Hooks: Memoization: https://medium.com/@sdolidze/react-hooks-memoization-99a9a91c8853

[22]

Hooks API Reference: https://reactjs.org/docs/hooks-reference.html

[23]

useCallback: https://reactjs.org/docs/hooks-reference.html#usecallback

[24]

Hooks API Reference: https://reactjs.org/docs/hooks-reference.html

[25]

Using a Custom Hook: https://reactjs.org/docs/hooks-custom.html#using-a-custom-hook

[26]

Rules of Hooks 鉤子的規則: https://reactjs.org/docs/hooks-rules.html

相關焦點

  • 寫React Hooks前必讀
    必開規則:{ "plugins": ["react-hooks"], "rules": { "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn" }}其中,react-hooks/exhaustive-deps
  • 函數式編程看React Hooks(一)簡單React Hooks實現
    以下 三點是 react 官網所提到的 hooks 的動機 https://zh-hans.reactjs.org/docs/hooks-intro.html#motivation代碼重用:在hooks出來之前,常見的代碼重用方式是 HOC 和render props,這兩種方式帶來的問題是:你需要解構自己的組件,同時會帶來很深的組件嵌套複雜的組件邏輯:在class組件中
  • React 16.8發布:hooks終於來了!
    hooks 可以讓你在不編寫類的情況下使用 state 和 React 的其他功能。你還可以構建自己的 hooks,在組件之間共享可重用的有狀態邏輯。你不一定要現在學習 hooks,它並沒有帶來重大變化,我們也沒有計劃從 React 中移除類。hooks 的 FAQ(https://reactjs.org/docs/hooks-faq.html)談到了 hooks 的逐步採用策略。我們不建議你為了能夠馬上採用 hooks 而對現有應用程式進行重大重寫。
  • React Hooks使用小結
    但是從掘金,知乎,博客以及面試等地方可以發現,hooks是無法避免的一個話題,好像不用hooks你就是上個世紀的人了(流下了前端卑微的淚水)  為了不當原始人,今天就來和大家一起探討下hooks大法。為什麼要用hooks?
  • 寫React Hooks前需要注意什麼?
    "react-hooks/rules-of-hooks": "error",    "react-hooks/exhaustive-deps": "warn"  }}其中, react-hooks/exhaustive-deps 至少warn,也可以是error。
  • 30 分鐘精通 React 新特性——React Hooks
    ——擁有了hooks,你再也不需要寫Class了,你的所有組件都將是Function。你還在為搞不清使用哪個生命周期鉤子函數而日夜難眠嗎?——擁有了Hooks,生命周期鉤子函數可以先丟一邊了。你在還在為組件中的this指向而暈頭轉向嗎?——既然Class都丟掉了,哪裡還有this?你的人生第一次不再需要面對this。
  • 【實戰總結篇】寫React Hooks前必讀
    "react-hooks/rules-of-hooks": "error",    "react-hooks/exhaustive-deps": "warn"  }}其中, react-hooks/exhaustive-deps 至少warn,也可以是error。
  • ahooks 正式發布:值得擁抱的 React Hooks 工具庫
    方案,這便也是 ahooks 的由來。那麼好奇的你肯定會問, ice/hooks 與標題的 ahooks 的關係是什麼?待我細細道來 😆在 ice/hooks RFC 期間,我們也對比參考了社區的同類方案諸如 react-use 等,但最終因為 react-use 提供的 Hooks 過於冗餘
  • 使用 React Hooks 的正確姿勢
    Hooks 解決了什麼問題Hooks 解決了我們五年來編寫和維護成千上萬的組件時遇到的各種各樣看起來不相關的問題。無論你正在學習 React,或每天使用,或者更願嘗試另一個和 React 有相似組件模型的框架,你都可能對這些問題似曾相識。React 沒有提供將可復用性行為「附加」到組件的途徑(例如,把組件連接到 store )。
  • 【譯】使用Enzyme和React Testing Library測試React Hooks
    原文:https://css-tricks.com/testing-react-hooks-with-enzyme-and-react-testing-library/當你開始在應用中使用React Hooks時,你需要確保編寫的代碼是可靠的。確保代碼沒有bug的一種方法就是編寫測試用例。
  • 你不知道的 React Hooks(萬字長文,快速入門必備)
    Hooks 本質上就是一類特殊的函數,它們可以為你的函數型組件(function component)注入一些特殊的功能,讓您在不編寫類的情況下使用 state(狀態) 和其他 React 特性。組成複雜難以維護: 複雜的組件中有各種難以管理的狀態和副作用,在同一個生命周期中你可能會因為不同情況寫出各種不相關的邏輯,但實際上我們通常希望一個函數隻做一件事情。
  • 超性感的React Hooks(四):useEffect
    許多朋友試圖利用class語法中的生命周期來類比理解useEffect,也許他們認為,hooks只是語法糖而已。那麼,即使正在使用hooks,也有可能對我上面這一段話表示不理解,甚至還會問:不類比生命周期,怎麼學習hooks?我不得不很明確的告訴大家,生命周期和useEffect是完全不同的。
  • 30 分鐘精通 React 今年最勁爆的新特性 —— React Hooks
    ——擁有了hooks,你再也不需要寫Class了,你的所有組件都將是Function。你還在為搞不清使用哪個生命周期鉤子函數而日夜難眠嗎? ——擁有了Hooks,生命周期鉤子函數可以先丟一邊了。你在還在為組件中的this指向而暈頭轉向嗎? ——既然Class都丟掉了,哪裡還有this?你的人生第一次不再需要面對this。
  • React v16.8 發布:帶來穩定版的 Hooks 功能
    hooks 可以讓你在不編寫類的情況下使用 state 和 React 的其他功能。你還可以構建自己的 hooks,在組件之間共享可重用的有狀態邏輯。>從 16.8.0 開始,React 包含穩定的 React Hooks 實現:React DOMReact DOM ServerReact Test RendererReact Shallow Renderer要注意的是,如需使用 hooks
  • 超性感的React Hooks(七)useReducer
    』, payload: 10})}>遞增B</Button> <Button onClick={() => dispatch({type: 『decrementB』, payload: 10})}>遞減B</Button> </div> );}這個簡單的例子就這樣實現了。
  • React Hooks 還不如類?
    我們發現,類可能是學習 React 道路上的一大障礙。你必須了解 this 在 JavaScript 中的工作機制,這和大多數語言中的機制截然不同。你必須記得綁定事件處理程序。你必須遵循一些嚴格而怪異的規則:https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level需要注意代碼放置的位置,並且這裡面存在許多陷阱。我不能將一個 hook 放在一個 if 語句中,因為 hooks 的內部機制是基於調用順序的,這簡直太瘋狂了!
  • React Hooks 原理與最佳實踐
    但當組件的樣式或者結構不滿足需求的時候,我們只能去重新實現這個組件。在我們開發 React 應用的時候,經常會遇到類似下面這種場景,你可能會有兩個疑問:對於高階組件來說,如果你沒有對組件手動設置 name/displayName,就會遇到更嚴重的問題,那就是一個個匿名組件嵌套。畢竟上面 render props 的嵌套至少能知道組件名。
  • 「不容錯過」手摸手帶你實現 React Hooks
    轉自手寫 React Hookshttps://juejin.im/post/6872223515580481544手寫 React Hooks Hooks 是 React 16.8 新增的特性,它可以讓你在不編寫 class 的情況下使用 state 以及其他的 React 特性凡是 use 開頭的 React API 都是 HooksHook
  • 超性感的React Hooks(三):useState
    </div>}函數式組件非常簡單,也正因如此,一些特性常常被忽略,而這些特性,是掌握React Hooks的關鍵。1.函數式組件接收props作為自己的參數import React from 'react';interface Props { name: string, age: number}function Demo({ name, age }: Props) { return [ <div>name: {
  • React hooks 最佳實踐【更新中】
    1.儘量設計簡單的hookshooks 設計的初衷就是為了使開發更加快捷簡便,因此在使用hooks 的時候,我們不應該吝嗇使用較多的hooks,例如我們處理不同狀態對應不同邏輯的時候,按照寫class的邏輯,我們經常會在一個生命周期函數裡寫下多個邏輯,並用if區分;在寫hooks的時候,因為沒有shouldComponentUpdate這類的生命周期函數,我們應該將他們分離開,將他們寫在不同的useEffect