All hooks / Component helpers

useControllableState

204 B minified and brotliedSourceMarkdown✓ Works in all modern browsers

State for a component that can be controlled (the parent passes value and onChange) or uncontrolled (the parent passes only a default), like a native <input> with value or defaultValue.

Returns [value, setValue] like useState. setValue calls onChange only when the value actually changes, and accepts an updater function that sees the latest value, also for several calls in one event. Controlled while value is not undefined; don't switch between the two.

Import

import { useControllableState } from '@vincent.ri/react-tils'
// or only this hook
import { useControllableState } from '@vincent.ri/react-tils/useControllableState'

Usage

type Props = { open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void }
function Disclosure({ open: openProp, defaultOpen = false, onOpenChange }: Props) {
  const [open, setOpen] = useControllableState(openProp, defaultOpen, onOpenChange)
  return <button aria-expanded={open} onClick={() => setOpen((o) => !o)}>Details</button>
}

Live demo

Same component: owns its state, or the parent does.

parent: closed

When to use it

Building a reusable component (tabs, accordion, toggle, select, dialog open) whose parent may or may not want to own the state.

When not to

The state is only used inside one component; use useState. Never copy a prop into state with useState(prop) + useEffect to sync it: the copy goes stale or flickers. Use this hook instead.

API

function useControllableState<T>(value: T | undefined, defaultValue: T, onChange?: (value: T) => void): [T, SetValue<T>]
ParameterDescription
valueThe controlled value, or undefined when uncontrolled.
defaultValueThe starting value when uncontrolled.
onChangeCalled with the new value on every change. Optional.

Browser support

Uses only long-established browser features, so it works wherever React does.

First version with support, from MDN's compatibility data (web-features 3.42.0, @mdn/browser-compat-data 8.1.5).

Coming from another library?

Other libraries call this useControlled, useUncontrolled, useControllableValue.