RE

Additional Hooks

React · 6 entries

useReducer

syntax
const [state, dispatch] = useReducer(reducer, initialState);
// reducer: (state, action) => newState
example
import { useReducer } from 'react';

function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM':
      return { ...state, items: [...state.items, action.payload] };
    case 'REMOVE_ITEM':
      return {
        ...state,
        items: state.items.filter(i => i.id !== action.payload),
      };
    case 'CLEAR':
      return { ...state, items: [] };
    default:
      throw new Error(`Unknown action: ${action.type}`);
  }
}

function ShoppingCart() {
  const [cart, dispatch] = useReducer(cartReducer, { items: [] });

  return (
    <div>
      <p>{cart.items.length} items</p>
      <button onClick={() =>
        dispatch({ type: 'ADD_ITEM', payload: { id: 1, name: 'Notebook' } })
      }>
        Add Notebook
      </button>
      <button onClick={() => dispatch({ type: 'CLEAR' })}>
        Clear Cart
      </button>
    </div>
  );
}

Note Prefer useReducer over useState when you have complex state logic with multiple sub-values, or when the next state depends on the previous state in varied ways. The reducer function must be pure -- no side effects.

useId

syntax
const id = useId();
example
import { useId } from 'react';

function LabeledInput({ label, type = 'text' }) {
  const id = useId();

  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input id={id} type={type} />
    </div>
  );
}

// Each instance gets a unique, stable ID
<LabeledInput label="Username" />
<LabeledInput label="Email" type="email" />

Note useId generates IDs that are stable across server and client renders, preventing hydration mismatches. Never use it for keys in a list. It is meant for accessibility attributes (htmlFor, aria-describedby, etc.).

useDeferredValue

syntax
const deferredValue = useDeferredValue(value);
example
import { useState, useDeferredValue, useMemo } from 'react';

function HeavySearch({ allItems }) {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);

  const results = useMemo(
    () => allItems.filter(item =>
      item.name.toLowerCase().includes(deferredQuery.toLowerCase())
    ),
    [allItems, deferredQuery]
  );

  const isStale = query !== deferredQuery;

  return (
    <div>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Search..."
      />
      <div style={{ opacity: isStale ? 0.6 : 1 }}>
        {results.map(item => <p key={item.id}>{item.name}</p>)}
      </div>
    </div>
  );
}

Note useDeferredValue lets the UI stay responsive by deferring re-rendering of expensive parts. It is similar to debouncing but integrated with React's rendering scheduler. Compare the deferred and current value to show a stale indicator.

useTransition

syntax
const [isPending, startTransition] = useTransition();
example
import { useState, useTransition } from 'react';

function TabContainer() {
  const [tab, setTab] = useState('home');
  const [isPending, startTransition] = useTransition();

  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }

  return (
    <div>
      <nav>
        <button onClick={() => selectTab('home')}>Home</button>
        <button onClick={() => selectTab('analytics')}>Analytics</button>
        <button onClick={() => selectTab('settings')}>Settings</button>
      </nav>
      {isPending && <p>Loading...</p>}
      <TabPanel activeTab={tab} />
    </div>
  );
}

Note startTransition marks state updates as non-urgent, allowing urgent updates (like typing) to interrupt them. The isPending flag lets you show loading indicators. Unlike useDeferredValue, useTransition wraps the state update itself.

useSyncExternalStore

syntax
const snapshot = useSyncExternalStore(
  subscribe,
  getSnapshot,
  getServerSnapshot? // optional, for SSR
);
example
import { useSyncExternalStore } from 'react';

function useOnlineStatus() {
  return useSyncExternalStore(
    (notify) => {
      window.addEventListener('online', notify);
      window.addEventListener('offline', notify);
      return () => {
        window.removeEventListener('online', notify);
        window.removeEventListener('offline', notify);
      };
    },
    () => navigator.onLine,  // client snapshot
    () => true               // server snapshot (assume online)
  );
}

function StatusBar() {
  const isOnline = useOnlineStatus();
  return <p>{isOnline ? 'Connected' : 'Offline'}</p>;
}

Note Designed for subscribing to external data sources (browser APIs, third-party stores) in a way that is compatible with concurrent rendering. The subscribe function must accept a callback and return an unsubscribe function.

useDebugValue

syntax
useDebugValue(value);
useDebugValue(value, formatFn); // lazy formatting
example
import { useDebugValue, useSyncExternalStore } from 'react';

function useMediaQuery(query) {
  const matches = useSyncExternalStore(
    (notify) => {
      const mql = window.matchMedia(query);
      mql.addEventListener('change', notify);
      return () => mql.removeEventListener('change', notify);
    },
    () => window.matchMedia(query).matches,
    () => false
  );

  // Shows in React DevTools next to the hook
  useDebugValue(matches ? `Matches: ${query}` : `No match: ${query}`);

  return matches;
}

Note useDebugValue only affects the React DevTools display for custom hooks. It adds no runtime behavior. Use the format function (second argument) to defer expensive string formatting until the hook is actually inspected in DevTools.