Aur UI

Use Controllable State

Use Controllable State from Aur UI.

bash
pnpm add @aur-ui/ui
tsx
import { useControllableState } from "@aur-ui/ui/hooks/use-controllable-state";
hooks/use-controllable-state.ts
"use client";

import { type SetStateAction, useCallback, useRef, useState } from "react";

/**
 * Minimal controlled/uncontrolled state hook. Lets a component accept either
 * `value` (controlled) or fall back to `defaultValue` (uncontrolled), always
 * firing `onChange` on updates.
 *
 * `setValue` accepts a plain value or a functional updater. Updaters resolve
 * against the latest value (tracked in a ref), so rapid successive updates in
 * one tick never read a stale closure — in either mode.
 *
 * Naming convention for consuming components: expose `value` / `defaultValue`
 * / `onValueChange`.
 */
export function useControllableState<T>(params: {
  value: T | undefined;
  defaultValue: T;
  onChange?: (value: T) => void;
}): [T, (next: SetStateAction<T>) => void] {
  const { value, defaultValue, onChange } = params;
  const isControlled = value !== undefined;
  const [uncontrolled, setUncontrolled] = useState(defaultValue);

  // Keep the latest onChange without forcing setValue to change identity.
  const onChangeRef = useRef(onChange);
  onChangeRef.current = onChange;

  const stateValue = isControlled ? value : uncontrolled;

  // Latest resolved value, for stale-closure-safe functional updaters.
  const valueRef = useRef(stateValue);
  valueRef.current = stateValue;

  const setValue = useCallback(
    (next: SetStateAction<T>) => {
      const resolved =
        typeof next === "function"
          ? (next as (prev: T) => T)(valueRef.current)
          : next;
      valueRef.current = resolved;
      if (!isControlled) setUncontrolled(resolved);
      onChangeRef.current?.(resolved);
    },
    [isControlled],
  );

  return [stateValue, setValue];
}