Post
2025年12月2日 下午09:31

狀態切換行為使用 React 19.2 ViewTransition 搭配 startTransition

前陣子把 nextjs 升級到 16 之後,最大的好處就是現在可以直接使用 React 19.2 的 ViewTransition Component 了。

過去的 Lightbox 雖然也有使用瀏覽器的 SPA view-transition API,但使用的方式有點獵奇。

在使用 ViewTransition 之後可以搭配 useTransition hook 的 startTransition 進行狀態及動畫的觸發管理,省掉不少時間。

但在搭配上一頁關閉圖片的功能時還是有點小問題沒辦法解決。startTransition 包裹的行為只是將相關 state 更新 defer 成(React 的)transition。不會包含由瀏覽器歷史紀錄(上一頁/下一頁)變更觸發的 state update。直接包裹會造成由使用者觸發的 mode update 被 defer,並且觸發歷史紀錄變更立即關閉 lightbox,使 React 觸發 view-transition 過渡動畫時狀態已經變更完畢了,沒有動畫可以播放。

解決做法其實也很簡單。將 closeLightbox 簡化到只負責觸發歷史紀錄變更,等瀏覽器變更完畢後觸發 startTransition 修改 mode、更新 layout、播放動畫。

use-gesture 及 event threshold

為了簡化 event binding,我們選擇使用簡單的 use-gesture。

事後才發現其實沒有簡化多少,可以完全自己來

這裡我們只簡單綁定幾個事件:

  • onClick:單擊關閉控制元件層、雙擊縮放
  • onDrag:拖曳(pan)圖片,根據縮放狀態不同有不同行為
  • onDragEnd:計算是否需要關閉 Lightbox,或是將圖片拉回 boundary 內。
  • onWheel/onWheelEnd 將 macOS 的觸控板事件綁到 onDrag/onDragEnd 上。實際上 onWheelEnd 是模擬事件,並沒有辦法精確知道手勢放開的時...
...顯示更多 (683 字)

offset 補償與 transform: translate/scale

為了使用瀏覽器本身的 HW accel,我們改用 transform: translate/scale 控制圖片位置。同時也把圖片的 CSS position 固定在 absolute,讓圖片在平常的垂直水平置中過渡到 freeform 之間的動畫不會因為 reflow 而卡頓。

不過,同時帶來一個小問題:scale 過後的圖片是由本身的 origin 為中心平移,圖片本身座標空間不能直接對應到畫面上的顯示座標空間。也就是說,double tap 跟 pinch 事件取得的座標不能夠直接用來更新 transform。

為了解...

...顯示更多 (327 字)

useTransform 與 MotionValue

use-gesture 的 handler 相對來說有效率,但不可避免的必須依賴 gesture-mode 對事件進行不同處理。

我們可以將目前圖片的 x, y 儲存在 useState 內交由 React 管理,不過我們實際上不需要因為圖片動了一個 pixel 就觸發 rerender。

我們也可以在各個 handler 內計算目前應該有的 gesture-mode,但這時除了邏輯重複外,也可能會有 race condition。

因此,這邊我們將 x, y 保留在 MotionValue 中,由 motion 管理動畫,並且藉由一個 useTransform...

...顯示更多 (648 字)