# Controlled & Uncontrolled Components Created: 2023_06_28 18:16 Tags: [[Software]] [[React]] [[JavaScript]] You may have heard of controlled & uncontrolled components in the context of inputs & forms in the React Docs ([Controlled](https://reactjs.org/docs/forms.html#controlled-components) & [Uncontrolled](https://reactjs.org/docs/uncontrolled-components.html)). But I have been using this pattern for more than just inputs, many of my components follow the pattern of being able to operate in a controlled way or an uncontrolled way. I'm not here to say that it is the best pattern, as it does introduce some complexity, but I find that it ultimately reduces complexity depending on where it is used. Before that, though, let me explain what controlled & uncontrolled components even are (in my eyes, as I may deviate from the React docs on this). ## Controlled Components Controlled components are components where the parent component must manage its state. This is a natural pattern for React: to "react" to changes in props and re-render the child component. Take these components, for example: ```jsx function Parent() { const [isOpen, setIsOpen] = React.useState(false); return ( <div> <Dropdown isOpen={isOepn} onChange={setIsOpen} /> Dropdown is {isOpen ? 'open' : 'closed'} </div> ); } function Dropdown({ isOpen, onChange }) { return ( <div> <button onClick={() => onChange(!isOpen)}>{isOpen ? 'Open' : 'Close'} Dropdown</button> {isOpen ? <div>Dropdown</div> : null} </div> ); } ``` In this example, the `Parent` component has complete control of the state of the dropdown, it can change the open state, and choose to update the open state when the dropdown calls the `onChange` function. This makes the `Dropdown` component completely "controlled". This does add some complexity to the parent component though, what if it doesn't actually need to control the dropdown in any way? ## Uncontrolled Components Uncontrolled components are components where the state is managed internally but provide ways of setting a default and let the parent component listen for changes in that state. This is not a pattern that I see used nearly often enough, but will be familiar to most. Note: In the new React docs, uncontrolled components is defined as the parent having no 'influence' on the child state, but I disagree with that definition. I think it can have influence, just not able to completely control it and update whenever. Take these components, for example: ```jsx function Parent() { const [isOpen, onChange] = React.useState(false); return ( <div> <Dropdown initialIsOpen={false} onChange={onChange} /> Dropdown is {isOpen ? 'open' : 'closed'} </div> ); } function Parent2() { return ( <div> <Dropdown initialIsOpen={false} /> Dropdown is ??? </div> ); } function Dropdown({ initialIsOpen, onChange }) { const [isOpen, setIsOpen] = React.useState(initialIsOpen); useEffect( () => { onChange && onChange(isOpen); }, // Would need to handle the `onChange` function being changed here // but in this case we know more that onChange is referentially stable [isOpen] ); return ( <div> <button onClick={() => setIsOpen(!isOpen)}>{isOpen ? 'Open' : 'Close'} Dropdown</button> {isOpen ? <div>Dropdown</div> : null} </div> ); } ``` In this version, the `Parent` component has some influence in the state of the dropdown, but only for the first render, and it can "follow-along" with state changes (via `onChange`) to know what the current state of the dropdown. `Parent2` is where you can really see the use of this: if the component doesn't need to know what the state is, then it doesn't even have to handle it. This shifts the complexity from the parent into the child. An example where it is useful to have both a controlled & uncontrolled component are in component libraries. With so many different use cases and large amounts of usage throughout the consuming application, the goal of a component library should be to offer a convenience to developers to have complete control when they need it & simple interfaces when they don't. Let's see an example: ```jsx function ParentThatWantsControl() { const [isOpen, onChange] = React.useState(false); return ( <div> <Dropdown isOpen={isOpen} onChange={onChange} /> Dropdown is {isOpen ? 'open' : 'closed'} <button onClick={() => onChange(open => !open)}>Toggle Dropdown</button> </div> ); } function ParentThatWantsSimplicity() { return ( <div> <Dropdown initialIsOpen={true} /> </div> ); } function ParentThatWantsToListenToChanges({ onClose }) { return ( <div> <Dropdown initialIsOpen={false} onChange={isOpen => { if (!isOpen) { onClose(); } }} /> </div> ); } ``` In each example, the Parent is using the simplest version of the `Dropdown` component to achieve what it needs. - `ParentThatWantsControl` can know & reset the state of the `Dropdown` - `ParentThatWantsSimplicity` can disregard the state of the `Dropdown` yet (optionally) have influence in the initial state that it should start in - `ParentThatWantsToListenToChanges` can listen to important state updates without actually having to manage the state for itself For component libraries, having a component that can be flexible enough to handle all of these scenarios would be pretty convenient as you could choose when calling the component how much control over it you would like to take on, while minimizing the amount of state in that component. So, how can we implement that? First, let's go over the rules: - The component will need to accept: `isOpen`, `initialIsOpen` and `onChange` as props - The component can only be controlled or uncontrolled for it's lifecycle otherwise things will get confusing for when each state is applied - If using `isOpen`: - The caller is choosing to manage the state entirely - `onChange` becomes required to allow the component to communicate state changes that happen within - `initialIsOpen` should be ignored - If using `initialIsOpen`: - The caller is choosing to not manage state - If `onChange` is provided: - Any state updates should be called back to it - If `isOpen` changes to an actual value: - Warn that the caller should handle state ## Hook Implementation Now that we know the rules, let's see a naive implementation: ```js const noop = () => {}; function useControlled({ value, initialValue, onChange = noop }) { const { current: isControlled } = React.useRef(value !== undefined); if (isControlled) { return [value, onChange]; } const [localValue, setValue] = React.useState(initialValue); return [ localValue, value => { setValue(value); onChange(value); } ]; } ``` > This implementation is for illustration purposes only, to highlight some points I'd like to make. There needs to be some criteria to pick whether a component is controlled or not, but it should be stable across renders. I chose to compare against `undefined` since that is the default for a prop that is not provided. From there the hook essentially operates in two modes, controlled and uncontrolled. ### Controlled A controlled hook basically just returns back the props you gave it as the hook should operate like: ```jsx function Parent() { const [state, setState] = React.useState(''); return <Child state={state} onChange={setState} />; } function Child({state, onChange}){ return <button onClick={()=>onChange(String(Date.now()))}>{state}</button>; } function ChildWithHook({state, onChange}){ const [value, setValue]=useControlled({value:state, onChange}); return <button onClick={()=>setValue(String(Date.now()))>{value}</button>; } ``` So, the `ChildWithHook` ends up just needing to send the props it got back to the caller. This allows us to do some nice optimizations because we do not have to allocate state for variables that we will not ever end up using (following the rule that a controlled component always stays controlled). ### Uncontrolled The uncontrolled version is where things get to be a bit more interesting. To be uncontrolled means to have local state, yet optionally callback to the parent saying what the current state is. There are several optimizations that can be made to this implementation like having a stable function reference for the setter like `useState` does, but now we are getting ahead of ourselves. ```jsx function Parent() { const [state, setState] = React.useState(''); return <Child state={state} onChange={setState} />; } function Child({ initialState, onChange }) { const [state, setState] = React.useState(initialState); return ( <button onClick={() => { const nextState = String(Date.now()); setState(nextState); onChange?.(nextState); }} > {state} </button> ); } function ChildWithHook({ initialState, onChange }) { const [value, setValue] = useControlled({ initialValue: initialState, onChange }); return <button onClick={() => setValue(String(Date.now()))}>{value}</button>; } ``` So, the hook saves us a bit of complexity within the child component (handling calling the `onChange` for us). But the real power of the hook is that it is just smoothing over where the state comes from and who needs to know what about it. Especially between the controlled and uncontrolled versions. ## Both! Now we can have something like this: ```jsx function Child({ initialState, state, onChange }) { const [value, setValue] = useControlled({ initialValue: initialState, value: state, onChange }); return <button onClick={() => setValue(String(Date.now()))}>{value}</button>; } function ParentWithControl() { const [state, setState] = React.useState(''); return ( <> <Child state={state} onChange={setState} /> <button onClick={() => setState('abc')}>Can reset state!</button> </> ); } function ParentWithoutControl() { return ( <Child initialState="" onChange={value => (value === 'abc' ? window.alert('Great guess!') : undefined)} /> ); } ``` The parents can use the component flexibly, control it's state if it wants to, or just listen for events if it wants to. ## When to use As usual, the answer will be that it depends. I feel like this pattern of using a component as controlled or uncontrolled is especially useful for generic components that are used in many places, like ones that would be in a design system (e.g. a checkbox). For these sorts of components, the additional API complexity is offset by its utility & flexibility. Do you really want to implement a toggle for each checkbox you use? Why doesn't it just handle it? With this pattern, it can. ## What is next? I'm probably going to publish a NPM package of an implementation that can be used in production. ## References -