# React ## 拆分global component共用邏輯成HOC ```javascript= // withAuth.js import { useEffect } from 'react'; const withAuth = WrappedComponent => { return function WithAuth(props) { useEffect(() => { const token = getUserToken(); // 您需要自行實現此函式 if (!token) { props.navigation.navigate('Login'); } }, []); return <WrappedComponent {...props} />; }; }; export default withAuth; // Home.js import withAuth from './withAuth'; function Home(props) { return ( <View> <Text>Home</Text> </View> ); } export default withAuth(Home); ``` ## 結合多個HOC ```javascript= import { compose } from 'redux'; import withAuth from './withAuth'; import withLogging from './withLogging'; import withAnalytics from './withAnalytics'; export default compose( withAuth, withLogging, withAnalytics )(MyComponent); ``` ## react async function to initial state ```javascript async function data() { const res = await fetch('https://jsonplaceholder.typicode.com/comments?_page=2').then(res => res.json()) return res } const value = await data() function About() { const [infos, setName] = useState(value) console.log({ infos }) return <div>Hello from About!</div> } ``` ## react-query with global state ```typescript= import React from 'react' import { useQuery } from '@tanstack/react-query' import { queryClient } from '@/App' const useRQGlobalState = (key: string, initionalState: any) => [ useQuery([key], () => initionalState, { initialData: initionalState }).data, (value: any) => queryClient.setQueryData([key], value) ] const StartEdit = () => { const [value, setValue] = useRQGlobalState('shortText', '111') return ( <input type="text" value={value} onChange={e => setValue(e.target.value)} /> ) } const StartView = () => { const [value, setValue] = useRQGlobalState('shortText', '111') return ( <p>{value}</p> ) } function GlobalState() { return ( <div>GlobalState <StartEdit /> <StartView /> </div> ) } export default GlobalState ``` ### Server components ![](https://i.imgur.com/hcXAbTi.png) https://react.dev/blog/2020/12/21/data-fetching-with-react-server-components https://chentsulin.medium.com/react-%E6%96%B0%E6%A6%82%E5%BF%B5-server-components-d632f9a18463 https://www.thearmchaircritic.org/mansplainings/react-server-components-vs-server-side-rendering https://oldmo860617.medium.com/%E5%BE%9E-next-js-13-%E8%AA%8D%E8%AD%98-react-server-components-37c2bad96d90 colocation :::info this is Info page ::: react fiber batch ## React ComponentProps ![螢幕截圖 2023-11-10 11.55.23](https://hackmd.io/_uploads/HkChYQsm6.png) ```typescript const atagAttribute = useRef<ComponentProps<'a'>>() ``` ## get Element ```typescript type DimensionInfo = { width: number, height: number } const useGetElementHeight = <ElementRef extends HTMLElement>( ref: MutableRefObject<ElementRef | null> ) => { const [dimension, setDimension] = useState<DimensionInfo>({ width: 0, height: 0 }) useLayoutEffect(() => { const getElementDimension = (): DimensionInfo => ({ width: ref.current?.offsetWidth ?? 0, height: ref.current?.offsetHeight ?? 0 }) const handleResize = () => { setDimension(getElementDimension()) } if (ref.current) { setDimension(getElementDimension()) } window.addEventListener('resize', handleResize) return () => { window.removeEventListener('resize', handleResize) } }, [ref]) return dimension } ``` ## react ref callback ```typescript const Form = () => { const [show, setShow] = React.useState(false) const [height, setHeight] = React.useState<number>(0) const ref = useCallback((node: HTMLInputElement | null) => { node?.focus() }, []) const h1Ref = useCallback((node: HTMLHeadingElement | null) => { if (node !== null) { setHeight(node.getBoundingClientRect().height) } }, []) return ( <form> <h1 ref={h1Ref}>123</h1> <p>height : {height}</p> <button type="button" onClick={() => setShow(!show)}> show </button> {{show && <input ref={ref} defaultValue="Hello world" />}} </form> ) } ``` ## React Internals Explorer https://jser.pro/ddir/rie?fbclid=PAZXh0bgNhZW0CMTEAAabb9HMcfQnwwmdoSkzwRywl9W- CpaWXEIUbw61qYY94Qg5-_1Plvi5ld8c_aem_NyzXjlfZs4KNBzhcrGS5DA ## Reset component state by key ```typescript function App() { const [value, setValue] = useState<number>(10) return ( <div> <Show value={value} key={value} /> <Set value={value} setValue={setValue} /> </div> ) } const Show = ({ value }: { value: number, }) => { const [text, setText] = useState<string>('') return ( <div> <p>text : {text}</p> <input type="text" value={text} onChange={e => setText(e.target.value)} /> <p>value : {value}</p> </div> ) } const Set = ({ setValue, value }: { value: number, setValue: React.Dispatch<React.SetStateAction<number>> }) => { return ( <div> <input type="number" onChange={e => setValue(Number(e.target.value))} value={value} /> </div> ) } ``` ## useCallBack with ref callBack 有一個前提在 `react` 中如果有使用 `useCallBack` 跟 `useMemo` 理論上有沒有使用它都不應該對產品產生不一樣的 `sideEffect` 畢竟這兩個只是 `enhancement` 的定位而已,而 `ref` 中有一個例外的情況是搭配 `useCallBack` 會有意想不到的結果,讓我們看以下的例子。 這邊當我們有一個情境需要 `scroll` 到特定的 `dom` 這樣的寫法會導致每次 `render` 都會 `scroll` ,因為 `ref callBack` 每次 `render` 都會執行,但顯然這不是我們要的,我們可能只希望 `scroll` 一次就好。 ```typescript function CustomInput() { const [value, setValue] = useState<number>(0) const ref = ((node) => { node?.scrollIntoView({ behavior: 'smooth' }) }) return ( <div> <button type='button' onClick={() => setValue(pre => pre + 1)}>add count {value}</button> <input ref={ref} defaultValue="Hello world" /> </div> ) } ``` 常見的解法就是包 `useCallBack` 但就像我們之前提到的 `useCallBack` 不應該是必要選項 ```typescript function CustomInput() { const [value, setValue] = useState<number>(0) const ref = React.useCallback((node) => { node?.scrollIntoView({ behavior: 'smooth' }) }, []) return ( <div> <button type='button' onClick={() => setValue(pre => pre + 1)}>add count {value}</button> <input ref={ref} defaultValue="Hello world" /> </div> ) } ``` 以目前的情境其實就是把它移除到 `component` 外面就好 ```typescript const ref = ((node) => { node?.scrollIntoView({ behavior: 'smooth' }) }) function CustomInput() { const [value, setValue] = useState<number>(0) return ( <div> <button type='button' onClick={() => setValue(pre => pre + 1)}>add count {value}</button> <input ref={ref} defaultValue="Hello world" /> </div> ) } ``` 定如果我們需要依賴 `useState` 呢?這時候就不得不放到 `component` 但以下的例子 `useCallBack` 可以拿掉,因為我們的 `height` 會是 `primitive type` 所以如果是相同值例如 `56` 這樣 `react` 會跳出渲染 ```typescript function MeasureExample() { const [height, setHeight] = React.useState(0) const measuredRef = React.useCallback(node => { if (node !== null) { setHeight(node.getBoundingClientRect().height) } }, []) return ( <> <h1 ref={measuredRef}>Hello, world</h1> <h2>The above header is {Math.round(height)}px tall</h2> </> ) } ``` 但如果不是 `primitive type` 你就不得不包 `useCallBack` 因為 `useState` 的 `ref` 儘管一樣單因為 `setState` 後 `ref` 改變導致重新渲染,所以這個例子來說建議用 `primitive type` 就好,原因在於之後 `react compiler` 的 `migrate` ```typescript function MeasureExample() { const [height, setHeight] = React.useState({height:0}) const measuredRef = React.useCallback(node => { if (node !== null) { setHeight({height: node.getBoundingClientRect().height}) } }, []) return ( <> <h1 ref={measuredRef}>Hello, world</h1> <h2>The above header is {Math.round(height)}px tall</h2> </> ) } ``` ## 延伸閱讀 上面用 `getBoundingClientRect` 會有一個問題是呼叫這個 `api` 會導致 [layout thrashing(佈局抖動)](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) 所以這邊建議所以這邊建議使用 `ResizeObserver` 拿取 `node` 相關資料,同時 `ref` 在 `react19` 有提供 `clean callBack` ```typescript function MeasureExample() { const [height, setHeight] = React.useState(0) const measuredRef = (node) => { const observer = new ResizeObserver(([entry]) => { setHeight(entry.contentRect.height) }) observer.observe(node) return () => { observer.disconnect() } } return ( <> <h1 ref={measuredRef}>Hello, world</h1> <h2>The above header is {Math.round(height)}px tall</h2> </> ) } ``` ## update ref when callnback change 這樣的好處是你可以不使用 `useCallBack` 對於日後 `react compiler` 野比較好 `migrate` ```typescript export function useHotkeys(hotkeys: Hotkey[]): { const hotkeysRef = useRef(hotkeys) useEffect(() => { hotkeysRef.current = hotkeys }) const onKeyDown = useCallback(() => ..., []) useEffect(() => { document.addEventListener('keydown', onKeyDown) return () => { document.removeEventListener('keydown', onKeyDown) } }, []) } ``` 等同於使用 `useCallBack` [資料來源](https://www.epicreact.dev/the-latest-ref-pattern-in-react) ```typescript export function useHotkeys(hotkeys: Hotkey[]): { const onKeyDown = useCallback(() => ..., [hotkeys]) useEffect(() => { document.addEventListener('keydown', onKeyDown) return () => { document.removeEventListener('keydown', onKeyDown) } }, [onKeyDown]) } ``` 或是使用 [useEffectEvent](https://react.dev/learn/separating-events-from-effects#declaring-an-effect-event) 但要注意這個還沒正式 `release` ,同時這邊的 `onKeyDown` 永遠都會是最新的 `callback` ```typescript export function useHotkeys(hotkeys: Hotkey[]): { const onKeyDown = useEffectEvent(() => ...) useEffect(() => { document.addEventListener('keydown', onKeyDown) return () => { document.removeEventListener('keydown', onKeyDown) } }, []) } ``` ## useEffectEvent 假設我們有一個需求是會希望根據不同的 `roomid` 去重新連結聊天室同時要確保每次 `theme` 更改的時候跳一個 `notification` 這時你會發現你的 `useEffect` 會有兩個 `dependency` ```typescript function Example() { const [theme, setTheme] = useState('light'); const [roomId, setRoomId] = useState('room-1'); useEffect(() => { console.log('%cconnect:'+roomId, 'background:green;'); console.log(theme); // notification with theme return () => { console.log('%cdisconnect:'+roomId, 'background:red;'); }; }, [roomId, theme]); return <div /> } ``` 但如果用 `useEffectEvent` 就可以讓 `theme` 脫離 `useEffect` 的 `dependency` 同時又可以保證每次都是 `theme` 得最新的 ```typescript function Example() { const [theme, setTheme] = useState('light'); const [roomId, setRoomId] = useState('room-1-2'); // 用 useEffectEvent 來包住函式 const onConnectSuccess = useEffectEvent(()=> { console.log(theme); // 可以正確取得 theme }) useEffect(() => { console.log('%cconnect:'+roomId, 'background:green;'); onConnectSuccess(); // 在這即可直接執行 useEffectEvent 回傳的函式 return () => { console.log('%cdisconnect:'+roomId, 'background:red;'); }; }, [roomId]); // 不用在放入 onConnectSuccess return <div /> } ``` ## useDeferredValue vs useDebounce 最大的差異在於 `useDeferredValue` 是取決於計算機的運算能力去 `delay ui render` , 但 `useDebounce` 永遠都會 `delay` ![image](https://hackmd.io/_uploads/rJrELew1-l.png) ## when to use useDeferredValue 簡單來說如果需要優化 `ui render` 效能用 `useDeferredValue` 準沒錯,`react` 會自動幫你根據 `cpu` 的結果換算 `delay` 時間。 ## when to use useDebounce 另外一個例子是,假設你需要減少 `api request` 次數的話用 `useDebounce` 比較合適,以下面的例子來說,會去把 `input` 做 `delay` 再去 `call api` ```typescript // usage <Form.Item<FieldsValue> label="Member Code" name="memberCode"> <MemberCodeAutoSelect /> </Form.Item> // MemberCodeAutoSelect.tsx export const MemberCodeAutoSelect = ({ onChange, ...props }: Pick<AutoCompleteProps, 'onChange' | 'value'>) => { const q = useDebounceState(props.value, 300); const { data: { pages: options = [] } = {}, fetchNextPage, isLoading, hasNextPage, isFetchingNextPage, } = useInfiniteQuery({ ...getMemberCodeFuzzySearchQuery({ q, page: 1, pageSize: 10, }), select: ({ pages, pageParams }) => ({ pages: pages.flatMap(page => page.data.data.map(code => ({ label: code, value: code, })), ), pageParams, }), refetchOnWindowFocus: false, }); return ( <AutoComplete {...props} // some props /> ); }; ``` 正常情況下儘管 `input` 打了很多次,最終只會 `call` 一次 `api` ![截圖 2025-11-04 上午11.51.17](https://hackmd.io/_uploads/rkcYdev1Wl.png) 但如果使用 `useDeferredValue` ```typescript // MemberCodeAutoSelect.tsx export const MemberCodeAutoSelect = ({ onChange, ...props }: Pick<AutoCompleteProps, 'onChange' | 'value'>) => { const q = useDeferredValue(props.value); const { data: { pages: options = [] } = {}, fetchNextPage, isLoading, hasNextPage, isFetchingNextPage, } = useInfiniteQuery({ ...getMemberCodeFuzzySearchQuery({ q, page: 1, pageSize: 10, }), select: ({ pages, pageParams }) => ({ pages: pages.flatMap(page => page.data.data.map(code => ({ label: code, value: code, })), ), pageParams, }), refetchOnWindowFocus: false, }); return ( <AutoComplete {...props} // some props /> ); }; 你會發現依舊會 `call` 多次的 `api`,因為當前的 `cpu` 運算很給力所以不會有 `delay` 情況 ``` ![截圖 2025-11-04 上午11.51.58](https://hackmd.io/_uploads/ryLTulvJ-g.png) ### 如何測試 `useDeferredValue` 可以在這邊調整 `cpu` 來測試 `useDeferredValue` 有沒有成功 ![image](https://hackmd.io/_uploads/r1f55KDJZg.png) ## Activity Reset `Next16` 開啟 `cacheComponent` 功能的時候預設會保留前一個頁面的 `dom` 節點,解決方式有以下兩種。 ### 1. Reset 整個 component 的 state ```typescript import { type PropsWithChildren, useLayoutEffect } from 'react'; type ActivityResetProps = PropsWithChildren<{ onReset: () => void }>; /** * ActivityReset component handles state reset when routes are wrapped with Activity * (used by Next.js Cache Components). * * When Next.js Cache Components are enabled, routes are wrapped with React's `<Activity>` * component, which preserves component state during client-side navigation. This means * components are not unmounted but set to "hidden" mode, preventing normal cleanup. * * This component uses useLayoutEffect's cleanup function to reset state when the * component becomes hidden, ensuring proper cleanup behavior. * * @example * ```tsx * <ActivityReset onReset={() => formStore.reset()}> * <YourForm /> * </ActivityReset> * ``` * * @see https://github.com/vercel/next.js/pull/85309/files * @see https://react.dev/reference/react/Activity#my-hidden-components-have-effects-that-arent-running * * Callback function to reset state when the component is hidden by Activity. * This is called in the cleanup function of useLayoutEffect to ensure proper * state reset when Next.js Cache Components (using React Activity) hides the route. * * @see https://react.dev/reference/react/Activity#my-hidden-components-have-effects-that-arent-running */ export const ActivityReset = ({ onReset, children }: ActivityResetProps) => { useLayoutEffect(() => { return () => { onReset(); }; }, []); return <>{children}</>; }; // 手動實作哪些 state 需要去 reset import { type PropsWithChildren } from 'react'; import { ActivityReset } from '@/components/ui/activity-reset'; import { useIsDirtyContext } from '@/utils/hooks/useIsDirty'; import { useStepContext } from '@/utils/hooks/useStep'; import { useFormStore } from './store/formStore'; export const RebateFormActivityReset = ({ children }: PropsWithChildren) => { const setIsDirty = useIsDirtyContext(store => store.setIsDirty); const firstForm = useFormStore(store => store.firstStepForm); const secondForm = useFormStore(store => store.secondStepForm); const onResetStep = useStepContext(store => store.onReset); const handleReset = () => { setIsDirty(false); firstForm.resetFields(); secondForm.resetFields(); onResetStep(); }; return <ActivityReset onReset={handleReset}>{children}</ActivityReset>; }; // 如此每次的 form instance 就不用擔心保留 state 的問題了 export const RebateFormProvider = ({ children, initialValues, ...formStoreConfigs }: RebateFormProviderProps) => { return ( <IsDirtyProvider> <StepProvider> <FormStoreProvider initialValues={initialValues} {...formStoreConfigs}> <RebateFormActivityReset>{children}</RebateFormActivityReset> </FormStoreProvider> </StepProvider> </IsDirtyProvider> ); }; ``` ### 2.另外一種就是用 key 去 render 如次每次使用的 `component` 就是最新的內容了 ```typescript 'use client'; import { usePathname } from 'next/navigation'; /** * ForceRemount component forces a complete remount of children when routes change. * * When Next.js Cache Components are enabled (Next.js 16+), routes are wrapped with * React's `<Activity>` component, which preserves component state during client-side * navigation. This means components are not unmounted but set to "hidden" mode, * preventing normal cleanup and causing DOM nodes to persist across route changes. * * This component uses the current pathname as a React key to force a complete * remount of the component tree when the route changes. When the key changes, * React will unmount the old component tree and mount a new one, ensuring * proper cleanup and fresh state for each route. * * @example * ```tsx * <ForceRemount> * <YourForm /> * </ForceRemount> * ``` * * @see https://github.com/vercel/next.js/pull/85309/files * @see https://react.dev/reference/react/Activity#my-hidden-components-have-effects-that-arent-running * * @param children - React nodes to be remounted when the route changes */ export const ForceRemount = ({ children }: { children: React.ReactNode }) => { const pathname = usePathname(); return <div key={pathname}>{children}</div>; }; // 把他包在你的 provider 最外層 export const CampaignFormProvider = ({ mode, initialValues, initialReadonly, children, }: CampaignFormProviderProps) => ( <ForceRemount> <IsDirtyProvider> <StepProvider> <FormStoreProvider initialReadonly={initialReadonly} initialValues={initialValues} mode={mode} > {children} </FormStoreProvider> </StepProvider> </IsDirtyProvider> </ForceRemount> ); ``` ## use Activity to prefetch 假設我還有一個 `AsyncComponent` ```typescript const AsyncComponent = () => { const { data } = useSuspenseQuery({ queryKey: ['async-component'], queryFn: () => { return new Promise((resolve) => { setTimeout(() => { resolve('Hello, world!'); }, 1000); }); }, }); return <div>{data}</div>; } ``` 通常如果要 `render condition` 你會這樣做,等到 `show` 的時候才去 `get data` ```typescript <Suspense fallback={<div>Loading...</div>}> {show && <AsyncComponent />} </Suspense> ``` 但如果搭配 `Activity` 可以提前先在背景 `prerender` 等到 `Activity` `show` 的時候 `AsyncComponent` 已經有 `data` 所以也不會先跑 `loading ui` ```typescript <Suspense fallback={<div>Loading...</div>}> <Activity mode={show ? 'visible' : 'hidden'}> <AsyncComponent /> </Activity> </Suspense> ``` 另一個有優化會是在 `ssr` `hydration` 的部分,假設當我們 `CommonComponent` 是主要內容 `PostComponent` 是次要的,常會因為要同時把兩個 `component` 去做 `render` 而影響 `hydration` 時間,這時候我們可以把次要內容用 `Activity` 包起來,這被 `Activity` 包起來的 `component` 就代表他會等到沒有被 `Activity` 包起來的內容也就是這邊的 `CommonComponent` `hydration` 完成後再去 `hydration` `PostComponent` ,這大大減少整體 `hydration` 時間 ```typescript <> <CommonComponent /> <Activity> <PostComponent /> </Activity> </> ``` ## Progress Bar ```typescript const [value, setValue] = useState<number>(0) useEffect(() => { const id = setInterval(() => { setValue(current => Math.min(100, Math.round((current + Math.random() * 10)))) }, 100) return () => clearInterval(id) }, []) ```