Skip to content

Add a shared useControllableState hook #876

Description

@ravisuhag

Summary

Components that support both controlled and uncontrolled usage each re-implement the controlled/uncontrolled merge by hand, with a slightly different shape every time. There's no shared hook for it in packages/raystack/hooks.

Current duplication

  • sidebar-root.tsx:128-129const [internalOpen, setInternalOpen] = useState(defaultOpen); const open = providedOpen ?? internalOpen;
  • select-root.tsx:113-122 — same ?? pattern, duplicated again for searchValue a few lines later.
  • data-view.tsx:73-83 — different style again: explicit isViewControlled = view !== undefined boolean instead of ??.

Components built directly on Base UI primitives (Checkbox, Switch, Tabs, Accordion, Dialog) don't have this problem — they get controlled/uncontrolled handling for free from Base UI. This only affects components with custom root logic.

Suggested approach

Add one useControllableState({ value, defaultValue, onChange }) hook to packages/raystack/hooks, modeled on @radix-ui/react-use-controllable-state. Migrate Sidebar, Select, and DataView onto it so all three follow the same merge semantics.

Notes

Found via a components.build practices audit (state management dimension).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestglobalCross-cutting issue affecting multiple components

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions