React 18 - 新的Hook useTransition === ![image](https://hackmd.io/_uploads/rJkSze1XR.png) --- **useTransition** 的主要目的是提供一種機制來標記某些狀態更新為“可中斷”,意思是說 React 可以再需要時暫停這些更新,先處理更重要的工作,從而保持應用的響應性。 簡單說就是 : 優化! 優化!再優化! ```jsx import { useTransition, useState } from 'react'; function App() { const [isPending, startTransition] = useTransition(); const [input, setInput] = useState(''); const handleChange = (e) => { const nextValue = e.target.value; startTransition(() => { setInput(nextValue); }); }; return ( <div> <input type="text" value={input} onChange={handleChange} /> {isPending ? <p>Loading...</p> : <p>You can see changes here.</p>} </div> ); } ``` 在這個官方的範例中: - 使用者輸入事件 **handleChange** 用 **startTransition** 包裝了 setInput 調用。 - 這表示這個狀態更新,,即使輸入欄位的內容更新也可以被延遲,簡單說就是不會被即時處理。React 可以選擇延遲處理輸入欄位的更新。 - **isPending** 用於追蹤過渡是否正在進行,來顯示載入狀態或其他回饋給用戶。 在這裡使用 **useTransition** 可以提高大型應用的性能,特別是在處理大量數據或在複雜界面中進行更新時,這樣用戶就不會感覺到明顯的卡頓或延遲。 --- ## useTransition 的底層運作 **useTransition**的底層實現是在 React 的 Concurrent Mode 中進行非緊急更新的一部分。 這種模式允許 React 在執行更新時保持介面的響應性。當使用 **useTransition** 標記某些狀態更新時,這些更新被視為非緊急並且是可以被中斷的。 ![image](https://hackmd.io/_uploads/Sk8397Jm0.png) **useTransition** 通過 **startTransition** 函數調用來標記這些過度。當調用 **startTransition**時,更新會被放入一個特定的 lane(通道),稱為 transition Lane。 這些 lane 擁有較低的優先級,使得React 可以選擇在處理更緊急的任務時推遲這些更新。 --- ## useWindowSize 的update: 在上一篇(https://hackmd.io/@myrealstory/rkv4TexbA )useWindowSize 統計整個page的DOM資訊裡,例如scrollY 會因為每動一次就會馬上進行updatemount 然而大量地消耗了網頁的效能。那我們可以透過 **useTransition** 來進行優化。 這裡就直接貼上更新的部分,如有不清楚建議閱讀useWindowSize 的篇章喔: Part1 : ```jsx export const useWindowSize = () =>{ const [isPending, startTransition] = useTransition(); const path = usePathname(); const locationForMoreThanTwoFolder = path.split( "/")[3]; ... ``` Part2: ```jsx //然後用 useEffect 來利用isPending 做更新initial 資料 const defaultWindowSizeState : WindowSizeProps = useMemo(()=>{ width : 0, height : 0, scrollY : 0, isWindowReady: false, }, []); // 一開始初始化 const [windowSize, setWindowSize] = useState(defaultWindowSizeState); //當isPending有更新,那就更新! useEffect(()=>{ if(!isPending) prevWindowSizeState.current = windowSize; },[ windowSize, isPending ]); // const debounceSetWindowState = debounce((state:WindowSizeProps) =>{ setWindowSize(state); },300); const setStateCallback = useCallback((state: WindowSizeProps)=>{ debounceSetWindowState(state); },[debounceSetWindowState]) ``` 在這裡一共套用了 **useTransition**, **debounce**, **useCallback** 來阻止即時運算,因為我們不需要時時刻刻地去監聽DOM的變化。 1. 使用 **useTransition** : - 被用來控制異步更新的優先級,特別是在有潛在的重渲染或資料密集的操作時。 - 通過 **startTransition** ,可以講某些更新標記為非緊急運算,這樣React可以在***處理其他更緊急的時間*** 時,暫停這些更新。 2. 使用 **debounce** : - 用於限制函數的調用頻率,避免因過於頻繁的更新(例如窗口大小改變時)而對性能造成負擔。 - 當窗口大小變化時,**debounce** 確保 **setWindowSize** 函數不會立即且頻繁地觸發,而是在指定的延遲時間後執行,這有助於降低不必要的渲染和計算,從而提高效能。 3. **useCallback** : - 用於將函數包裝成固定的參考值,這樣只要當 dependencies改變時,函數才會中心建立。 - 這樣可以防止了不必要的渲染。在以上的做法,useCallback用於包裝 setStateCallback函數,它依賴於 debounceSetWindowState,這確保了即時在組件多次渲染時,只要 **debounceSetWindowState 不變**, setStateCallback 函數都不會重新創建,從而節省資源。 ## 完整版本: ```jsx type WindowSizeProps = { width: number; height: number; scrollY: number; isWindowReady: boolean; }; export const useWindowSize = () => { const [isPending, startTransition] = useTransition(); const path = usePathname(); const locationForMoreThanTwoFolder = path.split("/")[3]; const slugs = getRouteNameFromPathname(path); let isForceHiddenPage = false; if (slugs.secondSlug === ROUTES.STORE_LOCATION || slugs.secondSlug === ROUTES.CHECKOUT || slugs.secondSlug === ROUTES.MAINTENANCE || slugs.secondSlug === ROUTES.MAINTENANCE_DAILY || locationForMoreThanTwoFolder === "payment-in-progress" || (slugs.secondSlug === ROUTES.CAMPAIGN && locationForMoreThanTwoFolder === "submitted")) { // condition copied from /src/components/BottomNavbar.tsx isForceHiddenPage = true; } const defaultWindowSizeState: WindowSizeProps = useMemo(() => ({ width: 0, height: 0, scrollY: 0, isWindowReady: false, }), []); const [windowSize, setWindowSize] = useState(defaultWindowSizeState); const [shouldBottomNavBarAppear, setShouldBottomNavBarAppear] = useState(isForceHiddenPage ? false : true); const lastScrollLocation = useRef<number>(0); const scrollTimeoutRef = useRef<NodeJS.Timeout | null>(null); const prevWindowSizeState = useRef(defaultWindowSizeState); useEffect(() => { if (!isPending) { prevWindowSizeState.current = windowSize; } }, [windowSize, isPending]) const debounceSetWindowState = debounce((state: WindowSizeProps) => { setWindowSize(state); }, 300); const setStateCallback = useCallback( (state: WindowSizeProps) => { debounceSetWindowState(state); }, [debounceSetWindowState] ) useEffect(() => { const handleScroll = () => { if (lastScrollLocation.current >= window.scrollY || window.scrollY <= 0) { // scrolling up keep display const shouldAppear = isForceHiddenPage ? false : true; if (shouldAppear !== shouldBottomNavBarAppear) { setShouldBottomNavBarAppear(shouldAppear); } } else { setShouldBottomNavBarAppear(false); } lastScrollLocation.current = window.scrollY; startTransition(() => { setStateCallback({ ...windowSize, scrollY: window.scrollY }); }) scrollTimeoutRef.current && clearTimeout(scrollTimeoutRef.current); scrollTimeoutRef.current = setTimeout(() => { // stop scrolling display setShouldBottomNavBarAppear(isForceHiddenPage ? false : true); }, 300); } const handleSize = () => { if (window.visualViewport) { startTransition(() => { setStateCallback({ ...windowSize, width: window?.visualViewport?.width ?? 0, height: window?.visualViewport?.height ?? 0, scrollY: window.scrollY, isWindowReady: true, }); }) } } if (typeof window !== "undefined") { window.addEventListener("resize", handleSize, { passive: true }); window.addEventListener("scroll", handleScroll, { passive: true }); const interval = setInterval(() => { if (!windowSize.isWindowReady) { startTransition(() => { setWindowSize({ width: window?.visualViewport?.width ?? 0, height: window?.visualViewport?.height ?? 0, scrollY: window.scrollY, isWindowReady: true, }); }) } }, 100) return () => { window.removeEventListener("resize", handleSize); window.removeEventListener("scroll", handleScroll); scrollTimeoutRef.current && clearTimeout(scrollTimeoutRef.current); clearInterval(interval); }; } }, [isForceHiddenPage, windowSize]); if (isPending) { return { ...prevWindowSizeState.current, isAppear: shouldBottomNavBarAppear, }; } return { ...windowSize, isAppear: shouldBottomNavBarAppear }; }; ``` 整體的設計解說可以回到上一篇閱讀。希望你們可以從這裡學習到什麼。感謝閱讀!Peace ![image alt](https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExYXg4Znc1MDNtYnZydXE0aDNxNmh0dnhscjU3YzhpcmJjaHVwMjJjaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/fxe8v45NNXFd4jdaNI/giphy.gif)