[譯] X 為啥不是 hook?
- 原文地址: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,也不用擔心狀態量的更新會相互干擾。
結論::white_check_mark: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 的返回值也是有跡可循的。
結論::white_check_mark: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 }) { // :warning: 不是真正的 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); // :warning: 不是真正的 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); // :warning: 不是真正的 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()
,不這樣做的話,它們就無法投出“反對票”來讓自己獲得更新。
結論::red_circle: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, 這個規模很恐怖。
這就像你要在抽屜裡找一把螺絲刀,而每一層抽屜裡都包含一堆小抽屜,你無法想象愛麗絲仙境中的兔子洞有多深。
結論::red_circle: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 、前端 、後端 、區塊鏈 、產品 、設計 、人工智慧 等領域,想要檢視更多優質譯文請持續關注掘金翻譯計劃 、官方微博、知乎專欄 。