Skip to Content
HooksuseControllableState

useControllableState

useControllableState lets design-system components support controlled and uncontrolled state with one consistent setter contract.

Live Example

Controllable component state

This demo shows one controlled value owned by the parent and one uncontrolled value owned by the hook.

Design system state

Controlled and uncontrolled controls

Notifications On

Controlled toggle

Parent value: true

Hook mode: Controlled

Uncontrolled select

Plan: team

Hook mode: Uncontrolled

Import

import { useControllableState } from "react-rsc-kit/client";

Signature

const [value, setValue, meta] = useControllableState<T>({ value, defaultValue, onChange, name, shouldUpdate, });

Parameters

NameTypeDefaultDescription
valueTundefinedControlled value. The hook is controlled when this is not undefined.
defaultValueT | () => TundefinedInitial uncontrolled value. Later changes to defaultValue are ignored.
onChange(nextValue: T, previousValue: T) => voidundefinedCalled when an accepted value change is requested.
namestringundefinedComponent or state name included in development warnings.
shouldUpdate(nextValue: T, previousValue: T) => booleanObject.isCustom predicate. Return true when the requested update should be applied.

Returns

ItemDescription
valueControlled value when controlled, otherwise internal uncontrolled state.
setValueReact-style setter supporting direct values and functional updates.
metaObject with isControlled, useful for diagnostics and component internals.

Controlled Toggle

"use client"; import { useControllableState } from "react-rsc-kit/client"; interface ControlledToggleProps { pressed: boolean; onPressedChange: (nextPressed: boolean, previousPressed: boolean) => void; } export function ControlledToggle({ pressed, onPressedChange }: ControlledToggleProps) { const [isPressed, setPressed] = useControllableState<boolean>({ value: pressed, onChange: onPressedChange, name: "Toggle.pressed", }); return ( <button type="button" aria-pressed={isPressed ?? false} onClick={() => setPressed((currentValue) => !currentValue)} > {isPressed ? "On" : "Off"} </button> ); }

Uncontrolled Toggle

"use client"; import { useControllableState } from "react-rsc-kit/client"; export function UncontrolledToggle() { const [isPressed, setPressed] = useControllableState<boolean>({ defaultValue: false, name: "Toggle.pressed", }); return ( <button type="button" aria-pressed={isPressed ?? false} onClick={() => setPressed((currentValue) => !currentValue)} > {isPressed ? "On" : "Off"} </button> ); }

Controlled Dialog

"use client"; import { useControllableState } from "react-rsc-kit/client"; interface DialogProps { open: boolean; onOpenChange: (nextOpen: boolean, previousOpen: boolean) => void; } export function Dialog({ open, onOpenChange }: DialogProps) { const [isOpen, setOpen] = useControllableState<boolean>({ value: open, onChange: onOpenChange, name: "Dialog.open", }); return ( <section hidden={!isOpen}> <button type="button" onClick={() => setOpen(false)}> Close </button> </section> ); }

Select Value

"use client"; import { useControllableState } from "react-rsc-kit/client"; interface SelectProps { value?: string; defaultValue?: string; onValueChange?: (nextValue: string, previousValue: string) => void; } export function Select({ value, defaultValue, onValueChange }: SelectProps) { const [selectedValue, setSelectedValue] = useControllableState<string>({ value, defaultValue, onChange: onValueChange, name: "Select.value", }); return ( <select value={selectedValue ?? ""} onChange={(event) => setSelectedValue(event.target.value)}> <option value="" disabled> Choose a value </option> <option value="react">React</option> <option value="typescript">TypeScript</option> <option value="design-system">Design System</option> </select> ); }

Notes

  • The hook is controlled when value !== undefined; undefined intentionally means “uncontrolled”.
  • defaultValue is read only for uncontrolled initialization.
  • setValue supports functional updates and calls the latest onChange callback.
  • shouldUpdate defaults to skipping updates where Object.is(nextValue, previousValue) is true.
  • Development warnings report controlled/uncontrolled mode switches and mixed value plus defaultValue usage.
  • Avoid switching a component between controlled and uncontrolled modes after mount.
Last updated on