- 原文地址:Why Isn’t X a Hook?
- 原文作者:Dan Abramov
- 譯文出自:掘金翻譯計劃
- 本文永久連結:github.com/xitu/gold-m…
- 譯者:Jerry-FD
- 校對者:yoyoyohamapi, CoolRice
由讀者翻譯的版本:西班牙語
自 React Hooks 第一個 alpha 版本釋出以來, 這個問題一直被激烈討論:“為什麼 API 不是 hook?”
你要知道,只有下面這幾個算是 hooks:
useState()
用來宣告 state 變數useEffect()
用來宣告副作用useContext()
用來讀取一些上下文
但是像 React.memo()
和 <Context.Provider>
,這些 API 它們不是 Hooks。一般來說,這些 Hook 版本的 API 被認為是 非元件化 或 反模組化 的。這篇文章將幫助你理解其中的原理。
注:這篇文章並非教你如何高效的使用 React,而是對 hooks API 饒有興趣的開發者所準備的深入分析。
以下兩個重要的屬性是我們希望 React 的 APIs 應該擁有的:
-
可組合:Custom Hooks(自定義 Hooks)極大程度上決定了 Hooks API 為何如此好用。我們希望開發者們經常使用自定義 hooks,這樣就需要確保不同開發者所寫的 hooks 不會衝突。(撰寫乾淨並且不會相互衝突的元件實在太棒了)
-
可除錯:隨著應用的膨脹,我們希望 bug 很容易被發現。React 最棒的特性之一就是,當你發現某些渲染錯誤的時候,你可以順著元件樹尋找,直到找出是哪一個元件的 props 或 state 的值導致的錯誤。
有了這兩個約束,我們就知道哪些算是真正意義上的 Hook,而哪些不算。
一個真正的 Hook: 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。
它不是一個 Hook: 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 是穩定的,但是這個特性顯然是與之相反了。
Debugging
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、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。