iT邦幫忙

2026 iThome 鐵人賽

DAY 30
0
Modern Web

再造輪子:30 天臨摹 React Hook 函式庫,探索背後的設計哲學系列 第 30 篇

【 Day 29 】下一次你打開一個函式庫,該先問什麼?

  • 分享至 

  • xImage
  •  

假使你正打算替專案挑一個函式庫;或者,剛在自己的程式碼裡寫好一個 Hook,正在想要不要讓第二個元件也用它。

打開 README,先看到的通常是功能清單:自動快取、跨分頁同步、支援伺服器渲染。再往下,是一段幾行就能跑起來的範例。這時候,我們很自然會問:它好不好上手?介面乾不乾淨?套件會不會太大?

這些問題回答的,比較像是用它的第一天會不會順利。

回頭看這一路,讓我卡住的地方,多半不在第一天,而是在第二個元件也要讀同一份值的時候、預設行為不合用的時候,或是想換掉一個元素的時候。

這個系列,我一直在問同一個問題:這個模組替呼叫端承擔了什麼,又把什麼留了下來?

到了昨天,這個問題也轉回了自己身上:輪到我們設計時,哪些判斷該由模組承擔,哪些又該交還給呼叫端?

這個問題很大,很難直接拿去問一份 README。讓我們回到這一路上,幾次讓我修正直覺的地方,看看在那裡,該先問的是什麼。

把資料搬出元件之後

探索遠端世界的第一天,兩個元件各自呼叫同一個 useTodos,主控台印出兩次「送出一次請求」。

修法看起來很直接:在元件外面放一個 Map,把早到的 Promise 存進去,後來的元件接上同一個。請求變成一次,問題似乎解決了。

接著,我們把兩個元件收起來,再打開。資料還在 Map 裡,沒有重抓。這通常是我們想要的;可是只收起其中一個元件時,可以把快取清掉嗎?另一個元件還在用,又該等到什麼時候才清?

那個 Map 回答不了。它從頭到尾沒有記下任何一個人,所以也不知道誰還在看。

把資料搬出元件,原本是為了讓兩個元件共用。搬出去的同時,這份資料也不再屬於任何一個元件。

原本可以跟著單一元件掛載、卸載來安排的事,現在得另外有人負責。

Day 07 把四條紅掉的斷言並排,得到的是同一句話:這個版本有快取,但快取沒有擁有者。TanStack Query 把生命週期做成快取裡的零件,替呼叫端記下有誰在看、何時算舊、何時回收;對照之下,在我們追的保留與回收問題上,SWR 預設的全域 Map 沒有附上一套隨最後一位讀取者離開而啟動的回收安排。

那個 Map 不一定是沒做完。保留資料、不隨元件離開而清除,也可以是一個選擇。只是如果應用需要另一種生命週期,呼叫端就得知道,這部分要由自己安排。

探索瀏覽器 API 時,同一件事又出現了一次。localStorage 裡的 theme 也不跟著任何一個元件卸載。瀏覽器替我們保存它,但什麼時候更新、什麼時候清掉,仍然需要有人決定。

所以下次看到「自動快取」或「全域共享」時,我想先問:這份值離開元件之後,它的生命週期由誰負責?

一個看起來不需要通知的值

使用者輸入的最後一天,form action 把值留在 <input> 裡。打字的時候,React 的元件樹沒有收到通知,只在送出的那一刻讀一次。

既然瀏覽器替我們保管的值,可以不必持續知道,那 localStorage 大概也一樣。要讀就 getItem,要寫就 setItem,中間似乎沒有誰需要被告知。

瀏覽器 API 的第一天,Day 16 把它包成一個 Hook,放進 ThemeSwitch 與 ThemeLabel 兩個元件裡。按下按鈕之後,ThemeSwitch 換成了 dark,負責顯示主題的 ThemeLabel 卻還停在 light。

localStorage 裡的值是對的。只是同一份值,有兩個元件在看,而寫入時只有其中一份副本被更新。

在 form action 範例裡,React 只需要在送出時讀取輸入值。theme 不一樣,兩個元件從掛載起,就一直把它顯示在畫面上。

要不要通知,得先看誰需要在什麼時候知道這份值。

Day 16 接著看的三個函式庫,差別正好就落在這件事上。react-use 寫入之後只更新自己;usehooks-ts 在 window 上廣播,收到的元件各自回頭重讀;@react-hookz/web 則以「一個 Storage 的一個 key」為單位,在模組層記下誰在看。

三個 Hook 的介面看起來差不多:給一個 key,拿回一個值與改它的方法。我原本以為瀏覽器 API 這一關不需要訂閱,結果分出它們深度的,正是訂閱。

Day 19 也說過,對一個只有一個元件在讀的值,react-use 這樣就夠了。需要多個元件同步時,「另一個元件怎麼跟上」才成為呼叫端需要處理的事。

所以在決定用哪一個,或決定自己的 Hook 要不要多做一點之前,我想先問:除了這個元件,還有誰在看同一份值?這個模組替我通知他們,還是留給我安排?

少寫的程式碼,原本在決定什麼

共享狀態的最後一天,Valtio 讓元件不必另外列出訂閱欄位:渲染時讀過哪些欄位,就訂閱哪些欄位。模組承擔得越多,呼叫端要寫的就越少。

這看起來是好事,直到我想知道它到底替我決定了什麼。兩個畫出一模一樣東西的元件,一個只解構出 cartCount,另一個把整份快照展開,訂閱就差了兩個欄位。

沒有哪一行寫錯。只是訂閱範圍沒有另外寫成一份清單,而是由渲染時實際讀過的欄位決定。

少寫了訂閱的宣告,那個決定仍然存在,只是換了一個地方發生。

互動行為那三天,相近的事情換了一種樣子出現。Radix 與 Base UI 把下拉選單做成一棵已經接好的元件樹,合約的 props 在元件裡就接上了元素。前兩天,在按鈕上多掛一個 onKeyDown,方向鍵就會安靜失效;到了這裡,預設用法中的接線由元件內部完成,呼叫端就少了一個可能覆蓋掉合約的位置。

可是這棵元件樹替我們決定的,只有接線嗎?

兩個函式庫都另外提供了一個打洞的 API:Radix 的 asChild,以及 Base UI 的 render。想要 <ul>/<li> 的時候,我們從這個入口,把元素的選擇拿回來。

也到了這時候,才更容易看見原本一起被包進去的事情。

在我們的實驗裡,替選項加上自己的 id,按鈕的 aria-activedescendant 就指向一個頁面上不存在的元素;改用函式形式的入口,展開 props 之後再寫一個 onKeyDown,方向鍵又打不開選單了。

原來那些少寫的程式碼,還包含識別如何對上、事件如何合併,以及行為如何接到元素上。用預設元件時,這些事在內部完成;換上自己的元素之後,其中一些責任又回到呼叫端。

Valtio 的訂閱範圍,與元件樹裡的接線,處理的是不同問題。它們讓我修正的,卻是同一個直覺:呼叫端沒有寫出來的事,不代表不需要決定。

所以看到「什麼都不用寫」時,我想先問:少寫的那些程式碼,原本是在做哪些決定?現在由誰替我決定?

預設不合用的那一天

真實 DOM 那四天,TanStack Virtual 與 react-virtuoso 都替呼叫端承擔了量測與位置之間的回饋。在我們的臨摹與測試情境裡,兩邊的迴圈也都停了下來。

照前面幾次的經驗,接下來應該是比較哪一邊承擔得比較多。

可是 Day 25 讓視窗之上的一列長高時,差別出現在另一個地方:scrollTop 該不該跟著補償?TanStack Virtual 把這個判斷做成公開的屬性,附上預設規則,呼叫端可以沿用,也可以替換;react-virtuoso 則把我們追的那條補償規則留在內部。

兩邊都深,深的形狀不同。

前一個問題,是認出模組替我們做了什麼決定。走到這裡,還得再問一步:知道它怎麼決定之後,如果我需要不同的答案呢?

這讓我想起瀏覽器 API 的 Day 18。另一個分頁刪除了值,@react-hookz/web 用一行 newValue !== null 的守衛把那個事件擋了下來。只看 Hook 的呼叫方式,看不出這個判斷,也沒有參數可以直接替換它。

模組承擔得多,仍然需要替各種情境做出判斷。對呼叫端來說,差別還包括:這個判斷留在內部,還是有一個公開入口,讓我們在需要時接手。

不過,有入口也不代表接手很容易。

要替換捲動補償的規則,得先理解什麼樣的尺寸變化應該補償,以及補償會如何影響正在看的位置。入口交出了決定權,做決定所需的知識,也成為呼叫端需要承擔的事。

預設合用的時候,這些都不必知道。通常要等預設不合用,這個問題才會變得迫切;也因此,在挑函式庫的第一天,我們很容易跳過它。

我想先問的是:預設的判斷不合用時,我能換掉它嗎?換掉之前,需要理解哪些條件?

四個問句

回頭看這幾個地方,每一次讓我改變想法的,都是同一件事:模組替呼叫端承擔了什麼,又把什麼留在了呼叫端手上。

下一次打開一個函式庫,或準備替自己的程式碼包上一層之前,我想先問這四句:

  • 這份值離開元件之後,它的生命週期由誰負責?
  • 除了這個元件,還有誰在看同一份值?這個模組替我通知他們,還是留給我安排?
  • 少寫的那些程式碼,原本是在做哪些決定?現在由誰替我決定?
  • 預設的判斷不合用時,我能換掉它嗎?換掉之前,需要理解哪些條件?

它們不太能拿來替函式庫分類,也排不出高下。同一個答案,放在只有一個元件的頁面裡可能剛好,放到需要多處同步的應用裡,就可能留下另一份工作。

它們也不一定都能從 README 得到答案。有時得多放一個元件,有時得換掉一個元素,有時則要讓預設規則碰上一個不合用的情境,才看得出責任落在哪裡。

這四句來自這一路上,我的直覺先走錯的地方。下次再走到那裡,先停下來問一次,至少能更早知道,自己接下的是什麼。

走到這裡,下一次打開函式庫時,我想帶著的問題已經有了。


由於我是第一次參與鐵人賽,一開始是從 Day 00 開賽的,今天是我的 Day 29,同時也是系列的第三十篇,我也試著用這篇文章為這次的鐵人賽收成句點。

不過我自己是期望這個系列還會有一篇的。

我不確定明天系統會不會讓我發文。如果可以,最後我想把目光從函式庫移回自己,看看一路問到這裡,我讀程式碼、理解設計的方式,有沒有發生變化。


上一篇
【 Day 28 】同一個問法,換了複雜度,答案會換形狀嗎?
下一篇
【 Day 30 】再造輪子之後
系列文
再造輪子:30 天臨摹 React Hook 函式庫,探索背後的設計哲學 共 31 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言