- 原文地址:Why Isn’t X a Hook?
- 原文做者:Dan Abramov
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:Jerry-FD
- 校對者:yoyoyohamapi, CoolRice
由讀者翻譯的版本:西班牙語html
自 React Hooks 第一個 alpha 版本發佈以來, 這個問題一直被激烈討論:「爲何 API 不是 hook?」前端
你要知道,只有下面這幾個算是 hooks:react
useState()
用來聲明 state 變量useEffect()
用來聲明反作用useContext()
用來讀取一些上下文可是像 React.memo()
和 <Context.Provider>
,這些 API 它們不是 Hooks。通常來講,這些 Hook 版本的 API 被認爲是 非組件化 或 反模塊化 的。這篇文章將幫助你理解其中的原理。android
注:這篇文章並不是教你如何高效的使用 React,而是對 hooks API 饒有興趣的開發者所準備的深刻分析。ios
如下兩個重要的屬性是咱們但願 React 的 APIs 應該擁有的:git
可組合:Custom Hooks(自定義 Hooks)極大程度上決定了 Hooks API 爲什麼如此好用。咱們但願開發者們常用自定義 hooks,這樣就須要確保不一樣開發者所寫的 hooks 不會衝突。(撰寫乾淨而且不會相互衝突的組件實在太棒了)github
可調試:隨着應用的膨脹,咱們但願 bug 很容易被發現。React 最棒的特性之一就是,當你發現某些渲染錯誤的時候,你能夠順着組件樹尋找,直到找出是哪個組件的 props 或 state 的值致使的錯誤。後端
有了這兩個約束,咱們就知道哪些算是真正意義上的 Hook,而哪些不算。api
useState()
多個自定義 Hooks 各自調用 useState()
不會衝突:安全
function useMyCustomHook1() {
const [value, setValue] = useState(0);
// 不管這裏作了什麼,它都只會做用在這裏
}
function useMyCustomHook2() {
const [value, setValue] = useState(0);
// 不管這裏作了什麼,它都只會做用在這裏
}
function MyComponent() {
useMyCustomHook1();
useMyCustomHook2();
// ...
}
複製代碼
無限制的調用一個 useState()
老是安全的。在你聲明新的狀態量時,你不用理會其餘組件用到的 Hooks,也不用擔憂狀態量的更新會相互干擾。
結論: ✅ useState()
不會使自定義 Hooks 變得脆弱。
Hooks 很是好用,由於你能夠在 Hooks 之間傳值:
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
// ...
return width;
}
function useTheme(isMobile) {
// ...
}
function Comment() {
const width = useWindowWidth();
const isMobile = width < MOBILE_VIEWPORT;
const theme = useTheme(isMobile);
return (
<section className={theme.comment}>
{/* ... */}
</section>
);
}
複製代碼
可是若是咱們的代碼出錯了呢?咱們又該怎麼調試?
咱們先假設,從 theme.comment
拿到的 CSS 的 class 是錯的。咱們該怎麼調試? 咱們能夠打一個斷點或者在咱們的組件體內加一些 log。
咱們可能會發現 theme
是錯的,可是 width
和 isMobile
是對的。這會提示咱們問題出在 useTheme()
內部。又或許咱們發現 width
自己是錯的。這能夠指引咱們去查看 useWindowWidth()
。
簡單看一下中間值就能指導咱們哪一個頂層的 Hooks 有 bug。 咱們不須要挨個去查看他們全部的實現。
這樣,咱們就可以洞察 bug 所在的部分,幾回三番以後,程序問題終得其解。
若是咱們的自定義 Hook 嵌套的層級加深的時候,這一點就顯得很重要了。假設一下咱們有一個 3 層嵌套的自定義 Hook,每一層級的內部又用了 3 個不一樣的自定義 Hooks。在 3 處找bug和最多 3 + 3×3 + 3×3×3 = 39 處找 bug 的區別是巨大的。幸運的是, useState()
不會魔法般的 「影響」 其餘 Hooks 或組件。與任何 useState()
所返回的變量同樣,一個可能形成 bug 的返回值也是有跡可循的。
結論: ✅ useState()
不會使你的代碼邏輯變得模糊不清,咱們能夠直接沿着麪包屑找到 bug。
useBailout()
做爲一個優化點,組件使用 Hooks 能夠避免重複渲染(re-rendering)。
其中一個方法是使用 React.memo()
包裹住整個組件。若是 props 和上次渲染完以後對比淺相等(shallowly equal),就能夠避免重複渲染。這和 class 模式中的PureComponent
很像。
React.memo()
接受一個組件做爲參數,並返回一個組件:
function Button(props) {
// ...
}
export default React.memo(Button);
複製代碼
但它爲何就不是 Hook?
不論你叫它 useShouldComponentUpdate()
、usePure()
、useSkipRender()
仍是 useBailout()
,它看起來都差很少長這樣:
function Button({ color }) {
// ⚠️ 不是真正的 API
useBailout(prevColor => prevColor !== color, color);
return (
<button className={'button-' + color}> OK </button>
)
}
複製代碼
還有一些其餘的變種 (好比:一個簡單的 usePure()
) 可是大致上來講,他們都有一些相同的缺陷。
咱們來試試把 useBailout()
放在 2 個自定義 Hooks 中:
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
// ⚠️ 不是真正的 API
useBailout(prevIsOnline => prevIsOnline !== isOnline, isOnline);
useEffect(() => {
const handleStatusChange = status => setIsOnline(status.isOnline);
ChatAPI.subscribe(friendID, handleStatusChange);
return () => ChatAPI.unsubscribe(friendID, handleStatusChange);
});
return isOnline;
}
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
// ⚠️ 不是真正的 API
useBailout(prevWidth => prevWidth !== width, width);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
});
return width;
}
複製代碼
譯註:使用了 useBailout
後,useFriendStatus
只會在 isOnline
狀態變化時才容許 re-render,useWindowWidth
只會在 width
變化時才容許 re-render。
如今若是你在同一個組件中同時用到他們會怎麼樣呢?
function ChatThread({ friendID, isTyping }) {
const width = useWindowWidth();
const isOnline = useFriendStatus(friendID);
return (
<ChatLayout width={width}>
<FriendStatus isOnline={isOnline} />
{isTyping && 'Typing...'}
</ChatLayout>
);
}
複製代碼
何時會 re-render 呢?
若是每個 useBailout()
的調用都有能力跳過此次更新,若是 useFriendStatus()
阻止了 re-render,那麼 useWindowWidth
就沒法得到更新,反之亦然。這些 Hooks 會相互阻塞。
然而,在組件內部,假若只有全部調用了 useBailout()
都贊成不 re-render 組件纔不會更新,那麼當 props 中的 isTyping
改變時,因爲內部全部 useBailout()
調用都沒有贊成更新,致使 ChatThread
也沒法更新。
基於這種假設,將致使更糟糕的局面,任何新置入組件的 Hooks 都須要去調用 useBailout()
,不這樣作的話,它們就沒法投出「反對票」來讓本身得到更新。
結論: 🔴 useBailout()
破壞了可組合性。添加一個 Hook 會破壞其餘 Hooks 的狀態更新。咱們但願這些 APIs 是穩定的,可是這個特性顯然是與之相反了。
useBailout()
對調試有什麼影響呢?
咱們用相同的例子:
function ChatThread({ friendID, isTyping }) {
const width = useWindowWidth();
const isOnline = useFriendStatus(friendID);
return (
<ChatLayout width={width}> <FriendStatus isOnline={isOnline} /> {isTyping && 'Typing...'} </ChatLayout> ); } 複製代碼
事實上即便 prop 上層的某處改變了,Typing...
這個 label 也不會像咱們指望的那樣出現。那麼咱們怎麼調試呢?
通常來講, 在 React 中你能夠經過向上尋找的辦法,自信的回答這個問題。 若是 ChatThread
沒有獲得新的 isTyping
的值, 咱們能夠打開那個渲染 <ChatThread isTyping={myVar} />
的組件,檢查 myVar
,諸如此類。 在其中的某一層, 咱們會發現要麼是容易出錯的 shouldComponentUpdate()
跳過了渲染, 要麼是一個錯誤的 isTyping
的值被傳遞了下來。一般來講查看這條鏈路上的每一個組件,已經足夠定位到問題的來源了。
然而, 假如這個 useBailout()
真是個 Hook,若是你不檢查咱們在 ChatThread
中用到的每個自定義 Hook (深刻地) 和在各自鏈路上的全部組件,你永遠都不會知道跳過此次更新的緣由。更由於任何父組件也可能會用到自定義 Hooks, 這個規模很恐怖。
這就像你要在抽屜裏找一把螺絲刀,而每一層抽屜裏都包含一堆小抽屜,你沒法想象愛麗絲仙境中的兔子洞有多深。
結論:🔴 useBailout()
不只破壞了可組合性,也極大的增長了調試的步驟和找 bug 過程的認知負擔 — 某些時候,是指數級的。
全文咱們探討了一個真正的 Hook,useState()
,和一個不太算是 Hook 的 useBailout()
,並從可組合性及可調試性兩個方面說明了爲何一個是 Hook,而一個不算是 Hook。
儘管如今沒有 「Hook 版本的 memo()
或 shouldComponentUpdate()
,但 React 確實提供了一個名叫 useMemo()
的 Hook。它有相似的做用,可是他的語義不會迷惑使用它的人。
useBailout()
這個例子,描述了控制組件是否 re-render 並不適合作成一個 hook。這裏還有一些其餘的例子 - 例如,useProvider()
, useCatch()
,useSuspense()
。
如今你知道爲何某些 API 不算是 Hook 了嗎?
(當你開始迷惑時,就提醒本身:可組合... 可調試)
Discuss on Twitter • Edit on GitHub
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。