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-129 — const [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).
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-129—const [internalOpen, setInternalOpen] = useState(defaultOpen); const open = providedOpen ?? internalOpen;select-root.tsx:113-122— same??pattern, duplicated again forsearchValuea few lines later.data-view.tsx:73-83— different style again: explicitisViewControlled = view !== undefinedboolean 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 topackages/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).