# 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://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

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

## 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`

但如果使用 `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` 情況
```

### 如何測試 `useDeferredValue`
可以在這邊調整 `cpu` 來測試 `useDeferredValue` 有沒有成功

## 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)
}, [])
```