State
How to manage state in a component, as well as merging controllable and uncontrolled state.
Building flexible components that work in both controlled and uncontrolled modes is a hallmark of professional components.
Uncontrolled State
Uncontrolled state is when the component manages its own state internally. This is the default usage pattern for most components.
For example, here's a simple Stepper
component that manages its own state internally:
import { useState } from 'react';
export const Stepper = () => {
const [value, setValue] = useState(0);
return (
<div>
<p>{value}</p>
<button onClick={() => setValue(value + 1)}>Increment</button>
</div>
);
};
Controlled State
Controlled state is when the component's state is managed by the parent component. Rather than keeping track of the state internally, we delegate this responsibility to the parent component.
Let's rework the Stepper
component to be controlled by the parent component:
type StepperProps = {
value: number;
setValue: (value: number) => void;
};
export const Stepper = ({ value, setValue }: StepperProps) => (
<div>
<p>{value}</p>
<button onClick={() => setValue(value + 1)}>Increment</button>
</div>
);
Merging states
The best components support both controlled and uncontrolled state. This allows the component to be used in a variety of scenarios, and to be easily customized.
Radix UI maintain an internal utility for merging controllable and uncontrolled state called use-controllable-state
. While not intended for public use, registries like Kibo UI have implemented this utility to build their own Radix-like components.
Let's install the hook:
npm install @radix-ui/react-use-controllable-state
This lightweight hook gives you the same state management patterns used internally by Radix UI's component library, ensuring your components behave consistently with industry standards.
The hook accepts three main parameters and returns a tuple with the current value and setter. Let's use it to merge the controlled and uncontrolled state of the Stepper
component:
import { useControllableState } from '@radix-ui/react-use-controllable-state';
type StepperProps = {
value: number;
defaultValue: number;
onValueChange: (value: number) => void;
};
export const Stepper = ({ value: controlledValue, defaultValue, onValueChange }: StepperProps) => {
const [value, setValue] = useControllableState({
prop: controlledValue, // The controlled value prop
defaultProp: defaultValue, // Default value for uncontrolled mode
onChange: onValueChange, // Called when value changes
});
return (
<div>
<p>{value}</p>
<button onClick={() => setValue(value + 1)}>Increment</button>
</div>
);
}