ResourcesReact Patterns7 React Hooks You Probably Haven't Used (But Should)
⚛️React Patterns7 React Hooks You Probably Haven't Used (But Should)7 min

7 React Hooks You Probably Haven't Used (But Should)

Beyond useState and useEffect, these React hooks solve real problems that trip up developers who don't know they exist.

📅January 22, 2026TechTwitter.ioreacthookspatternsadvanced

The Hooks Most Developers Miss

useState, useEffect, and useRef cover 80% of use cases. The other hooks in React's standard library solve specific problems so cleanly that reinventing them manually is always worse.


1. useReducer — Complex State Logic

When useState becomes an unwieldy mess of related state variables:

// Before: 4 useState calls that should move together
const [status, setStatus] = useState('idle')
const [data, setData] = useState(null)
const [error, setError] = useState(null)
const [retries, setRetries] = useState(0)

// After: useReducer with discriminated union state
type State =
  | { status: 'idle' }
  | { status: 'loading'; retries: number }
  | { status: 'success'; data: User }
  | { status: 'error'; error: Error; retries: number }

type Action =
  | { type: 'fetch' }
  | { type: 'success'; data: User }
  | { type: 'error'; error: Error }
  | { type: 'retry' }

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case 'fetch':   return { status: 'loading', retries: 0 }
    case 'success': return { status: 'success', data: action.data }
    case 'error':   return { status: 'error', error: action.error, retries: 0 }
    case 'retry':   return { status: 'loading', retries: state.status === 'error' ? state.retries + 1 : 0 }
  }
}

const [state, dispatch] = useReducer(reducer, { status: 'idle' })

2. useId — Unique, Stable IDs

Generating IDs for accessibility attributes (linking labels to inputs) used to require Math.random() or libraries:

// useId generates a stable, unique ID — even with SSR
function FormField({ label, ...props }) {
  const id = useId()
  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input id={id} {...props} />
    </div>
  )
}

// Safe to render multiple instances — each gets a unique ID
<FormField label="Email" type="email" />
<FormField label="Name" type="text" />

Unlike Math.random(), useId produces the same ID on server and client — no hydration mismatch.


3. useDeferredValue — Non-Urgent Updates

Keep the UI responsive while expensive re-renders catch up:

function SearchResults({ query }) {
  const deferredQuery = useDeferredValue(query)
  const isStale = query !== deferredQuery

  return (
    <div style={{ opacity: isStale ? 0.6 : 1 }}>
      {/* ExpensiveList only re-renders after the deferred value updates */}
      <ExpensiveList query={deferredQuery} />
    </div>
  )
}

The input updates instantly; the expensive list re-renders after the browser is free. No debouncing required.


4. useTransition — Mark Updates as Non-Urgent

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

  function handleTabClick(tab) {
    startTransition(() => {
      setTab(tab)  // React can interrupt this if something more urgent comes in
    })
  }

  return (
    <div>
      <TabButton onClick={() => handleTabClick('posts')} pending={isPending}>
        Posts
      </TabButton>
      <TabContent tab={tab} />
    </div>
  )
}

Updates inside startTransition are interruptible. If the user clicks another tab before the first renders, React abandons the first render.


5. useImperativeHandle — Controlled Ref Exposure

When you build a component that needs to expose an imperative API:

interface VideoPlayerRef {
  play: () => void
  pause: () => void
  seekTo: (seconds: number) => void
}

const VideoPlayer = forwardRef<VideoPlayerRef, Props>((props, ref) => {
  const videoRef = useRef<HTMLVideoElement>(null)

  useImperativeHandle(ref, () => ({
    play:   () => videoRef.current?.play(),
    pause:  () => videoRef.current?.pause(),
    seekTo: (s) => { if (videoRef.current) videoRef.current.currentTime = s },
  }))

  return <video ref={videoRef} {...props} />
})

// Usage — typed and controlled
const playerRef = useRef<VideoPlayerRef>(null)
playerRef.current?.seekTo(30)

The parent gets only the API you expose — not the raw DOM element.


6. useSyncExternalStore — Subscribe to External Data

The right way to subscribe to stores, browser APIs, or any external data source:

// Subscribe to online/offline status
function useIsOnline() {
  return useSyncExternalStore(
    (callback) => {
      window.addEventListener('online', callback)
      window.addEventListener('offline', callback)
      return () => {
        window.removeEventListener('online', callback)
        window.removeEventListener('offline', callback)
      }
    },
    () => navigator.onLine,      // getSnapshot (client)
    () => true,                   // getServerSnapshot (SSR)
  )
}

// Works with Redux, Zustand, or any external store
function useCounterStore() {
  return useSyncExternalStore(
    counterStore.subscribe,
    counterStore.getSnapshot,
  )
}

7. useLayoutEffect — Synchronous DOM Reads

Like useEffect, but fires synchronously after DOM updates and before the browser paints:

function Tooltip({ target, children }) {
  const tooltipRef = useRef(null)
  const [position, setPosition] = useState({ top: 0, left: 0 })

  useLayoutEffect(() => {
    // Measure the target and tooltip BEFORE browser paints
    // This prevents the tooltip from flashing in the wrong position
    const targetRect = target.getBoundingClientRect()
    const tooltipRect = tooltipRef.current.getBoundingClientRect()
    setPosition({
      top: targetRect.top - tooltipRect.height,
      left: targetRect.left,
    })
  }, [target])

  return (
    <div ref={tooltipRef} style={position}>
      {children}
    </div>
  )
}

Use useLayoutEffect when you need to measure DOM elements and reposition before paint. Use useEffect for everything else.


Key Takeaways

  • useReducer — complex state with multiple related values and transitions
  • useId — SSR-safe unique IDs for accessibility
  • useDeferredValue — keep input responsive while expensive renders catch up
  • useTransition — mark state updates as non-urgent and interruptible
  • useImperativeHandle — expose a controlled API from child to parent via refs
  • useSyncExternalStore — subscribe to external data sources safely
  • useLayoutEffect — synchronous DOM measurement before browser paint