React
useControllableState
Controlled and uncontrolled state behind a single interface.
Behaviour
Without a value, it keeps its own state from the default
const screen = await render(<Counter />);
await userEvent.click(screen.getByRole("button", { name: "Add" }));
await expect.element(screen.getByRole("status")).toHaveTextContent("1");With a value, it shows that value and only asks to change it
const onChange = vi.fn();
const screen = await render(<Counter value={5} onChange={onChange} />);
await userEvent.click(screen.getByRole("button", { name: "Add" }));
expect(onChange).toHaveBeenCalledWith(6);
await expect.element(screen.getByRole("status")).toHaveTextContent("5");A controlled value follows its owner
function Owner() {
const [value, setValue] = useState(10);
return <Counter value={value} onChange={setValue} />;
}
const screen = await render(<Owner />);
await userEvent.click(screen.getByRole("button", { name: "Add" }));
await expect.element(screen.getByRole("status")).toHaveTextContent("11");onChange hears changes in either mode, and not a set to the same value
const onChange = vi.fn();
const screen = await render(<Counter onChange={onChange} />);
await userEvent.click(screen.getByRole("button", { name: "Same" }));
await userEvent.click(screen.getByRole("button", { name: "Add" }));
expect(onChange.mock.calls).toEqual([[1]]);React
Install
npx shadcn@latest add https://ui.publictheta.com/r/use-controllable-state.jsonSource
src/primitives/use-controllable-state.ts
import { useCallback, useState } from "react";
/**
* State a parent may own. Passing `value` (anything but `undefined`) makes it
* controlled; otherwise it starts at `defaultValue` and is kept here. Either way
* `onChange` hears every change, and only real changes.
*/
export function useControllableState<T>(
value: T | undefined,
defaultValue: T,
onChange?: (value: T) => void,
): [T, (next: T) => void] {
const [internal, setInternal] = useState(defaultValue);
const controlled = value !== undefined;
const current = controlled ? value : internal;
const set = useCallback(
(next: T) => {
if (Object.is(next, current)) return;
if (!controlled) setInternal(next);
onChange?.(next);
},
[controlled, current, onChange],
);
return [current, set];
}