React 18 - 新的Hook useTransition
===

---
**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** 標記某些狀態更新時,這些更新被視為非緊急並且是可以被中斷的。

**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
