use-controllable-state.ts
Overview
The use-controllable-state.ts file provides a custom React hook, useControllableState, designed to manage state that can be either controlled or uncontrolled. This pattern is common in component libraries where a component's state can be controlled externally via props or managed internally by the component itself.
The hook abstracts the logic to switch between controlled and uncontrolled modes seamlessly, ensuring callback synchronization when the state changes. It improves component flexibility and reusability by allowing state management to be customized based on usage context.
Exports
useControllableState
function useControllableState<T>(params: UseControllableStateParams<T>): readonly [T | undefined, React.Dispatch<React.SetStateAction<T | undefined>>];
A custom React hook that manages a state value which can either be controlled externally (via the prop parameter) or uncontrolled internally (using defaultProp). It also accepts an onChange callback that fires when the state changes.
Types
UseControllableStateParams<T>
Parameters object for useControllableState hook.
Property | Type | Description |
|---|---|---|
| `T \ | undefined` (optional) |
| `T \ | undefined` (optional) |
|
| Callback invoked when the state changes. |
SetStateFn<T>
type SetStateFn<T> = (prevState?: T) => T;
A function type used to update state based on previous state, similar to the updater function in React’s setState.
Functions
useUncontrolledState
function useUncontrolledState<T>({
defaultProp,
onChange,
}: Omit<UseControllableStateParams<T>, 'prop'>): [T | undefined, React.Dispatch<React.SetStateAction<T | undefined>>];
A helper hook used internally by useControllableState to manage uncontrolled state.
Parameters:
defaultProp: Initial state value used if uncontrolled.onChange: Callback invoked when the state changes.
Returns:
A tuple:
[value, setValue]wherevalueis the current uncontrolled state andsetValueis the state updater function.
Implementation Details:
Uses
React.useStateto hold the internal state.Uses a
useRefto keep track of the previous state value.Calls the
onChangecallback inside auseEffectwhenever the uncontrolled state value changes.Uses
useCallbackRefto ensure stable callback references.
Example Usage:
const [value, setValue] = useUncontrolledState({
defaultProp: 'default',
onChange: (val) => console.log('Changed to', val),
});
useControllableState
function useControllableState<T>({
prop,
defaultProp,
onChange = () => {},
}: UseControllableStateParams<T>): readonly [T | undefined, React.Dispatch<React.SetStateAction<T | undefined>>];
Main hook exported by the file. Manages a state that can be controlled externally or internally.
Parameters:
prop: Controlled state value. If provided (notundefined), the state is considered controlled.defaultProp: Default value used for uncontrolled state initialization.onChange: Callback fired when the state changes.
Returns:
A tuple
[value, setValue]:value: The current state value (either controlled or internal).setValue: Function to update the state. Handles controlled vs uncontrolled updates appropriately.
Implementation Details:
Internally calls
useUncontrolledStateto manage uncontrolled mode.Determines control mode by checking if
propisundefined.If controlled, updates trigger
onChangebut do not update internal state.If uncontrolled, updates set the internal state.
Uses
useCallbackReffor stableonChangehandler references.Supports functional updates (i.e., passing a function to
setValue).
Example Usage:
// Controlled usage
const [value, setValue] = useControllableState<string>({
prop: controlledValue,
onChange: (val) => console.log('Controlled changed to', val),
});
// Uncontrolled usage
const [value, setValue] = useControllableState<string>({
defaultProp: 'initial',
onChange: (val) => console.log('Uncontrolled changed to', val),
});
Important Implementation Details
Controlled vs Uncontrolled:
The hook treats the presence of
prop(notundefined) as the controlled mode.In controlled mode, the state comes directly from
prop.In uncontrolled mode, internal
useStatemanages the state, initialized bydefaultProp.
Stable Callback Handling:
Uses
useCallbackRefto wraponChangecallbacks to avoid issues with stale closures or unnecessary re-renders.
State Updating Logic:
When setting state in controlled mode, the hook calls
onChangeonly if the new value differs from the currentprop.When setting state in uncontrolled mode, it updates the internal state, which triggers the
onChangecallback via auseEffecthook inuseUncontrolledState.
React Hook Dependencies:
useCallbackdependencies includeisControlled,prop,setUncontrolledProp, andhandleChangeto ensure correct update logic.
Interactions with Other Parts of the System
This hook depends on a utility hook
useCallbackRef(imported from@/hooks/use-callback-ref), which is used to maintain stable references to callback functions.It is likely used internally by UI components that want to support both controlled and uncontrolled state patterns, such as form inputs, toggles, or other interactive controls.
Because it is a generic hook, it can be reused extensively across components needing flexible state management.
The file references an upstream implementation from Radix UI primitives, indicating it is based on a well-tested and popular pattern.
Visual Diagram
flowchart TD
A[useControllableState<T>] --> B{Is prop defined?}
B -- Yes --> C[Controlled Mode]
B -- No --> D[Uncontrolled Mode]
D --> E[useUncontrolledState<T>]
E --> F[useState<T | undefined>]
E --> G[useEffect calls onChange on state change]
C --> H[Returns prop as value]
D --> I[Returns internal state as value]
A --> J[setValue function]
J --> K{Controlled?}
K -- Yes --> L[Call onChange if value differs]
K -- No --> M[Update internal state via setUncontrolledProp]
Summary
This file exports a single, versatile React hook useControllableState that simplifies dual-mode (controlled/uncontrolled) state management for React components. It abstracts the complexity of managing internal state vs external props and synchronizes all changes with an optional onChange callback. The hook leverages React's hooks and a stable callback ref utility to ensure consistent behavior and performance.
This utility is essential in component libraries that want to provide flexible APIs allowing consumers to choose their preferred state management style without rewriting component logic.