{"slug":"react-advanced-hooks","title":"React Advanced Hooks, Patterns, and Performance Optimization","tags":["react","hooks","typescript","performance","state-management","patterns"],"agent_summary":"Advanced React patterns — custom hooks for async state, useMemo/useCallback optimization, useReducer for complex state, data fetching with SWR/TanStack Query, context with selectors, portals, and concurrent mode patterns.","trigger_phrases":["React hooks","custom hook","useMemo","useCallback","useReducer","React performance","React context","React patterns","SWR","TanStack Query"],"runnable":false,"markdown":"\n## Overview\n\nAdvanced React patterns for production applications. Covers state management beyond useState, performance optimization, data fetching, and composable custom hooks.\n\n## Custom Hook: Async State\n\n```typescript\ntype AsyncState<T> =\n  | { status: \"idle\" }\n  | { status: \"loading\" }\n  | { status: \"success\"; data: T }\n  | { status: \"error\"; error: Error };\n\nfunction useAsync<T>(asyncFn: () => Promise<T>, deps: unknown[] = []) {\n  const [state, setState] = useState<AsyncState<T>>({ status: \"idle\" });\n\n  useEffect(() => {\n    let cancelled = false;\n    setState({ status: \"loading\" });\n\n    asyncFn()\n      .then((data) => {\n        if (!cancelled) setState({ status: \"success\", data });\n      })\n      .catch((error) => {\n        if (!cancelled) setState({ status: \"error\", error: error as Error });\n      });\n\n    return () => { cancelled = true; };\n  }, deps);\n\n  return state;\n}\n\n// Usage\nfunction UserProfile({ userId }: { userId: string }) {\n  const state = useAsync(() => fetchUser(userId), [userId]);\n\n  if (state.status === \"loading\") return <Skeleton />;\n  if (state.status === \"error\") return <ErrorMessage error={state.error} />;\n  if (state.status === \"success\") return <Profile user={state.data} />;\n  return null;\n}\n```\n\n## useReducer for Complex State\n\n```typescript\ntype State = {\n  items: Item[];\n  selectedId: string | null;\n  filter: \"all\" | \"active\" | \"archived\";\n  isLoading: boolean;\n};\n\ntype Action =\n  | { type: \"SET_ITEMS\"; items: Item[] }\n  | { type: \"SELECT\"; id: string }\n  | { type: \"DESELECT\" }\n  | { type: \"SET_FILTER\"; filter: State[\"filter\"] }\n  | { type: \"SET_LOADING\"; loading: boolean };\n\nfunction reducer(state: State, action: Action): State {\n  switch (action.type) {\n    case \"SET_ITEMS\":\n      return { ...state, items: action.items, isLoading: false };\n    case \"SELECT\":\n      return { ...state, selectedId: action.id };\n    case \"DESELECT\":\n      return { ...state, selectedId: null };\n    case \"SET_FILTER\":\n      return { ...state, filter: action.filter };\n    case \"SET_LOADING\":\n      return { ...state, isLoading: action.loading };\n    default:\n      return state;\n  }\n}\n\nfunction ItemList() {\n  const [state, dispatch] = useReducer(reducer, {\n    items: [],\n    selectedId: null,\n    filter: \"all\",\n    isLoading: false,\n  });\n\n  const filtered = useMemo(\n    () => state.items.filter(item =>\n      state.filter === \"all\" || item.status === state.filter\n    ),\n    [state.items, state.filter]\n  );\n\n  return (/* ... */);\n}\n```\n\n## useMemo and useCallback: When to Use\n\n```typescript\n// useMemo: expensive computations\nconst sortedItems = useMemo(() => {\n  return [...items].sort((a, b) => a.name.localeCompare(b.name));\n}, [items]);  // only recompute when items changes\n\n// useCallback: stable function reference for child components\nconst handleDelete = useCallback((id: string) => {\n  dispatch({ type: \"DELETE\", id });\n}, []);  // stable reference — doesn't cause unnecessary re-renders in children\n\n// When NOT to memoize (premature optimization):\n// - Simple value derivations\n// - Functions passed to DOM elements (not child components)\n// - Values used only once\n```\n\n## Context with Selector Pattern\n\nAvoid unnecessary re-renders by splitting context or using selectors:\n\n```typescript\n// Split contexts by update frequency\nconst UserDataContext = createContext<User | null>(null);\nconst UserActionsContext = createContext<UserActions | null>(null);\n\nfunction UserProvider({ children }: { children: React.ReactNode }) {\n  const [user, setUser] = useState<User | null>(null);\n\n  const actions = useMemo(() => ({\n    updateName: (name: string) => setUser(u => u ? { ...u, name } : u),\n    logout: () => setUser(null),\n  }), []);\n\n  return (\n    <UserActionsContext.Provider value={actions}>\n      <UserDataContext.Provider value={user}>\n        {children}\n      </UserDataContext.Provider>\n    </UserActionsContext.Provider>\n  );\n}\n\n// Consumers only re-render when their specific slice changes\nfunction UserName() {\n  const user = useContext(UserDataContext);\n  return <span>{user?.name}</span>;\n}\n\nfunction LogoutButton() {\n  const { logout } = useContext(UserActionsContext)!;\n  return <button onClick={logout}>Logout</button>;  // stable reference = no re-render on user change\n}\n```\n\n## TanStack Query (Data Fetching)\n\n```typescript\nimport { useQuery, useMutation, useQueryClient } from \"@tanstack/react-query\";\n\nfunction ProjectList() {\n  const { data, isLoading, error } = useQuery({\n    queryKey: [\"projects\"],\n    queryFn: () => fetch(\"/api/projects\").then(r => r.json()),\n    staleTime: 5 * 60 * 1000,  // 5 minutes\n  });\n\n  const queryClient = useQueryClient();\n  const createProject = useMutation({\n    mutationFn: (name: string) =>\n      fetch(\"/api/projects\", { method: \"POST\", body: JSON.stringify({ name }) }),\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: [\"projects\"] });\n    },\n  });\n\n  if (isLoading) return <Spinner />;\n  if (error) return <Error />;\n\n  return (\n    <div>\n      {data.map(p => <ProjectCard key={p.id} project={p} />)}\n      <button onClick={() => createProject.mutate(\"New Project\")}>\n        Create\n      </button>\n    </div>\n  );\n}\n```\n\n## Custom Hook: Local Storage State\n\n```typescript\nfunction useLocalStorage<T>(key: string, initialValue: T) {\n  const [value, setValue] = useState<T>(() => {\n    if (typeof window === \"undefined\") return initialValue;\n    try {\n      const stored = window.localStorage.getItem(key);\n      return stored ? JSON.parse(stored) : initialValue;\n    } catch {\n      return initialValue;\n    }\n  });\n\n  const setStoredValue = useCallback((newValue: T | ((prev: T) => T)) => {\n    setValue(prev => {\n      const resolved = typeof newValue === \"function\"\n        ? (newValue as (prev: T) => T)(prev)\n        : newValue;\n      localStorage.setItem(key, JSON.stringify(resolved));\n      return resolved;\n    });\n  }, [key]);\n\n  return [value, setStoredValue] as const;\n}\n```\n\n## Custom Hook: Debounced Value\n\n```typescript\nfunction useDebounce<T>(value: T, delayMs: number): T {\n  const [debounced, setDebounced] = useState(value);\n\n  useEffect(() => {\n    const timer = setTimeout(() => setDebounced(value), delayMs);\n    return () => clearTimeout(timer);\n  }, [value, delayMs]);\n\n  return debounced;\n}\n\n// Usage: debounce search input\nfunction SearchBox() {\n  const [query, setQuery] = useState(\"\");\n  const debouncedQuery = useDebounce(query, 300);\n\n  // Only fires 300ms after user stops typing\n  useEffect(() => {\n    if (debouncedQuery) fetchResults(debouncedQuery);\n  }, [debouncedQuery]);\n}\n```\n\n## Portal Pattern\n\n```typescript\nimport { createPortal } from \"react-dom\";\n\nfunction Modal({ isOpen, onClose, children }: ModalProps) {\n  if (!isOpen) return null;\n\n  return createPortal(\n    <div className=\"modal-overlay\" onClick={onClose}>\n      <div className=\"modal-content\" onClick={e => e.stopPropagation()}>\n        {children}\n      </div>\n    </div>,\n    document.getElementById(\"modal-root\")!\n  );\n}\n```\n\n## Performance Checklist\n\n```\n[ ] Large lists use virtualization (react-window or tanstack-virtual)\n[ ] Images lazy-loaded and sized (next/image)\n[ ] Bundle split at route level (Next.js default, manual with lazy())\n[ ] No anonymous object/array literals in JSX props (creates new ref each render)\n[ ] Context value memoized with useMemo\n[ ] Event handlers from useCallback when passed to memoized children\n[ ] React.memo on components with expensive render and stable props\n```\n","html":"<h2>Overview</h2>\n<p>Advanced React patterns for production applications. Covers state management beyond useState, performance optimization, data fetching, and composable custom hooks.</p>\n<h2>Custom Hook: Async State</h2>\n<pre><code class=\"language-typescript\">type AsyncState&#x3C;T> =\n  | { status: \"idle\" }\n  | { status: \"loading\" }\n  | { status: \"success\"; data: T }\n  | { status: \"error\"; error: Error };\n\nfunction useAsync&#x3C;T>(asyncFn: () => Promise&#x3C;T>, deps: unknown[] = []) {\n  const [state, setState] = useState&#x3C;AsyncState&#x3C;T>>({ status: \"idle\" });\n\n  useEffect(() => {\n    let cancelled = false;\n    setState({ status: \"loading\" });\n\n    asyncFn()\n      .then((data) => {\n        if (!cancelled) setState({ status: \"success\", data });\n      })\n      .catch((error) => {\n        if (!cancelled) setState({ status: \"error\", error: error as Error });\n      });\n\n    return () => { cancelled = true; };\n  }, deps);\n\n  return state;\n}\n\n// Usage\nfunction UserProfile({ userId }: { userId: string }) {\n  const state = useAsync(() => fetchUser(userId), [userId]);\n\n  if (state.status === \"loading\") return &#x3C;Skeleton />;\n  if (state.status === \"error\") return &#x3C;ErrorMessage error={state.error} />;\n  if (state.status === \"success\") return &#x3C;Profile user={state.data} />;\n  return null;\n}\n</code></pre>\n<h2>useReducer for Complex State</h2>\n<pre><code class=\"language-typescript\">type State = {\n  items: Item[];\n  selectedId: string | null;\n  filter: \"all\" | \"active\" | \"archived\";\n  isLoading: boolean;\n};\n\ntype Action =\n  | { type: \"SET_ITEMS\"; items: Item[] }\n  | { type: \"SELECT\"; id: string }\n  | { type: \"DESELECT\" }\n  | { type: \"SET_FILTER\"; filter: State[\"filter\"] }\n  | { type: \"SET_LOADING\"; loading: boolean };\n\nfunction reducer(state: State, action: Action): State {\n  switch (action.type) {\n    case \"SET_ITEMS\":\n      return { ...state, items: action.items, isLoading: false };\n    case \"SELECT\":\n      return { ...state, selectedId: action.id };\n    case \"DESELECT\":\n      return { ...state, selectedId: null };\n    case \"SET_FILTER\":\n      return { ...state, filter: action.filter };\n    case \"SET_LOADING\":\n      return { ...state, isLoading: action.loading };\n    default:\n      return state;\n  }\n}\n\nfunction ItemList() {\n  const [state, dispatch] = useReducer(reducer, {\n    items: [],\n    selectedId: null,\n    filter: \"all\",\n    isLoading: false,\n  });\n\n  const filtered = useMemo(\n    () => state.items.filter(item =>\n      state.filter === \"all\" || item.status === state.filter\n    ),\n    [state.items, state.filter]\n  );\n\n  return (/* ... */);\n}\n</code></pre>\n<h2>useMemo and useCallback: When to Use</h2>\n<pre><code class=\"language-typescript\">// useMemo: expensive computations\nconst sortedItems = useMemo(() => {\n  return [...items].sort((a, b) => a.name.localeCompare(b.name));\n}, [items]);  // only recompute when items changes\n\n// useCallback: stable function reference for child components\nconst handleDelete = useCallback((id: string) => {\n  dispatch({ type: \"DELETE\", id });\n}, []);  // stable reference — doesn't cause unnecessary re-renders in children\n\n// When NOT to memoize (premature optimization):\n// - Simple value derivations\n// - Functions passed to DOM elements (not child components)\n// - Values used only once\n</code></pre>\n<h2>Context with Selector Pattern</h2>\n<p>Avoid unnecessary re-renders by splitting context or using selectors:</p>\n<pre><code class=\"language-typescript\">// Split contexts by update frequency\nconst UserDataContext = createContext&#x3C;User | null>(null);\nconst UserActionsContext = createContext&#x3C;UserActions | null>(null);\n\nfunction UserProvider({ children }: { children: React.ReactNode }) {\n  const [user, setUser] = useState&#x3C;User | null>(null);\n\n  const actions = useMemo(() => ({\n    updateName: (name: string) => setUser(u => u ? { ...u, name } : u),\n    logout: () => setUser(null),\n  }), []);\n\n  return (\n    &#x3C;UserActionsContext.Provider value={actions}>\n      &#x3C;UserDataContext.Provider value={user}>\n        {children}\n      &#x3C;/UserDataContext.Provider>\n    &#x3C;/UserActionsContext.Provider>\n  );\n}\n\n// Consumers only re-render when their specific slice changes\nfunction UserName() {\n  const user = useContext(UserDataContext);\n  return &#x3C;span>{user?.name}&#x3C;/span>;\n}\n\nfunction LogoutButton() {\n  const { logout } = useContext(UserActionsContext)!;\n  return &#x3C;button onClick={logout}>Logout&#x3C;/button>;  // stable reference = no re-render on user change\n}\n</code></pre>\n<h2>TanStack Query (Data Fetching)</h2>\n<pre><code class=\"language-typescript\">import { useQuery, useMutation, useQueryClient } from \"@tanstack/react-query\";\n\nfunction ProjectList() {\n  const { data, isLoading, error } = useQuery({\n    queryKey: [\"projects\"],\n    queryFn: () => fetch(\"/api/projects\").then(r => r.json()),\n    staleTime: 5 * 60 * 1000,  // 5 minutes\n  });\n\n  const queryClient = useQueryClient();\n  const createProject = useMutation({\n    mutationFn: (name: string) =>\n      fetch(\"/api/projects\", { method: \"POST\", body: JSON.stringify({ name }) }),\n    onSuccess: () => {\n      queryClient.invalidateQueries({ queryKey: [\"projects\"] });\n    },\n  });\n\n  if (isLoading) return &#x3C;Spinner />;\n  if (error) return &#x3C;Error />;\n\n  return (\n    &#x3C;div>\n      {data.map(p => &#x3C;ProjectCard key={p.id} project={p} />)}\n      &#x3C;button onClick={() => createProject.mutate(\"New Project\")}>\n        Create\n      &#x3C;/button>\n    &#x3C;/div>\n  );\n}\n</code></pre>\n<h2>Custom Hook: Local Storage State</h2>\n<pre><code class=\"language-typescript\">function useLocalStorage&#x3C;T>(key: string, initialValue: T) {\n  const [value, setValue] = useState&#x3C;T>(() => {\n    if (typeof window === \"undefined\") return initialValue;\n    try {\n      const stored = window.localStorage.getItem(key);\n      return stored ? JSON.parse(stored) : initialValue;\n    } catch {\n      return initialValue;\n    }\n  });\n\n  const setStoredValue = useCallback((newValue: T | ((prev: T) => T)) => {\n    setValue(prev => {\n      const resolved = typeof newValue === \"function\"\n        ? (newValue as (prev: T) => T)(prev)\n        : newValue;\n      localStorage.setItem(key, JSON.stringify(resolved));\n      return resolved;\n    });\n  }, [key]);\n\n  return [value, setStoredValue] as const;\n}\n</code></pre>\n<h2>Custom Hook: Debounced Value</h2>\n<pre><code class=\"language-typescript\">function useDebounce&#x3C;T>(value: T, delayMs: number): T {\n  const [debounced, setDebounced] = useState(value);\n\n  useEffect(() => {\n    const timer = setTimeout(() => setDebounced(value), delayMs);\n    return () => clearTimeout(timer);\n  }, [value, delayMs]);\n\n  return debounced;\n}\n\n// Usage: debounce search input\nfunction SearchBox() {\n  const [query, setQuery] = useState(\"\");\n  const debouncedQuery = useDebounce(query, 300);\n\n  // Only fires 300ms after user stops typing\n  useEffect(() => {\n    if (debouncedQuery) fetchResults(debouncedQuery);\n  }, [debouncedQuery]);\n}\n</code></pre>\n<h2>Portal Pattern</h2>\n<pre><code class=\"language-typescript\">import { createPortal } from \"react-dom\";\n\nfunction Modal({ isOpen, onClose, children }: ModalProps) {\n  if (!isOpen) return null;\n\n  return createPortal(\n    &#x3C;div className=\"modal-overlay\" onClick={onClose}>\n      &#x3C;div className=\"modal-content\" onClick={e => e.stopPropagation()}>\n        {children}\n      &#x3C;/div>\n    &#x3C;/div>,\n    document.getElementById(\"modal-root\")!\n  );\n}\n</code></pre>\n<h2>Performance Checklist</h2>\n<pre><code>[ ] Large lists use virtualization (react-window or tanstack-virtual)\n[ ] Images lazy-loaded and sized (next/image)\n[ ] Bundle split at route level (Next.js default, manual with lazy())\n[ ] No anonymous object/array literals in JSX props (creates new ref each render)\n[ ] Context value memoized with useMemo\n[ ] Event handlers from useCallback when passed to memoized children\n[ ] React.memo on components with expensive render and stable props\n</code></pre>\n"}