Overview
The useControlled hook manages values that can work in either controlled or uncontrolled mode. This pattern is commonly used in form components like inputs, checkboxes, and switches, allowing them to work with or without external state management.
When a value prop is provided, the component operates in controlled mode. When only defaultValue is provided, it maintains its own internal state (uncontrolled mode).
Usage
import { useControlled } from '@kivora/react';
function CustomInput({ value, defaultValue = '', onChange }) {
const [currentValue, setValue] = useControlled({
value,
defaultValue,
onChange,
});
return (
<input
value={currentValue}
onChange={(e) => setValue(e.target.value)}
/>
);
}
Parameters
Accepts an object with the following properties:
The controlled value. When provided, the component operates in controlled mode
The initial value for uncontrolled mode. Used when value is undefined
onChange
(value: T) => void | undefined
Callback function invoked when the value changes. Called in both controlled and uncontrolled modes
Returns
Returns a tuple matching React’s useState API:
The current value (either controlled or internal state)
Function to update the value. Calls onChange and updates internal state if uncontrolled
Examples
Custom Toggle Component
import { useControlled } from '@kivora/react';
interface ToggleProps {
value?: boolean;
defaultValue?: boolean;
onChange?: (checked: boolean) => void;
}
function Toggle({ value, defaultValue = false, onChange }: ToggleProps) {
const [checked, setChecked] = useControlled({
value,
defaultValue,
onChange,
});
return (
<button
role="switch"
aria-checked={checked}
onClick={() => setChecked(!checked)}
style={{
background: checked ? 'green' : 'gray',
padding: '8px 16px',
borderRadius: '4px',
}}
>
{checked ? 'ON' : 'OFF'}
</button>
);
}
// Uncontrolled usage
<Toggle defaultValue={false} onChange={(checked) => console.log(checked)} />
// Controlled usage
const [enabled, setEnabled] = useState(false);
<Toggle value={enabled} onChange={setEnabled} />
Custom Select Component
import { useControlled } from '@kivora/react';
interface SelectProps<T> {
options: T[];
value?: T;
defaultValue: T;
onChange?: (value: T) => void;
renderOption: (option: T) => React.ReactNode;
}
function Select<T>({
options,
value,
defaultValue,
onChange,
renderOption
}: SelectProps<T>) {
const [selected, setSelected] = useControlled({
value,
defaultValue,
onChange,
});
return (
<div>
{options.map((option, index) => (
<button
key={index}
onClick={() => setSelected(option)}
style={{
fontWeight: option === selected ? 'bold' : 'normal',
}}
>
{renderOption(option)}
</button>
))}
</div>
);
}
Custom Counter Component
import { useControlled } from '@kivora/react';
interface CounterProps {
value?: number;
defaultValue?: number;
onChange?: (value: number) => void;
min?: number;
max?: number;
}
function Counter({
value,
defaultValue = 0,
onChange,
min = -Infinity,
max = Infinity,
}: CounterProps) {
const [count, setCount] = useControlled({
value,
defaultValue,
onChange,
});
const increment = () => {
if (count < max) setCount(count + 1);
};
const decrement = () => {
if (count > min) setCount(count - 1);
};
return (
<div>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
);
}
// Uncontrolled with limits
<Counter defaultValue={5} min={0} max={10} />
// Controlled
const [value, setValue] = useState(0);
<Counter value={value} onChange={setValue} />
Type Definition
interface UseControlledOptions<T> {
value: T | undefined;
defaultValue: T;
onChange: ((value: T) => void) | undefined;
}
function useControlled<T>(
options: UseControlledOptions<T>
): [T, (next: T) => void];
Notes
- This hook is primarily used internally by Kivora form components
- The hook automatically detects whether the component should be controlled or uncontrolled based on whether
value is undefined
- The
onChange callback is called in both controlled and uncontrolled modes
- The returned setter function is stable and won’t cause unnecessary re-renders