Refactoring controlled and uncontrolled components

If you've worked with React for a good amount of time, chances are you've heard the terms "uncontrolled" and "controlled" components. The React docs even have a couple sections dedicated to explaining these concepts. Most examples I've seen use form inputs to demonstrate these concepts, which is great because they are a perfect use case. But this pattern can and should be applied all throughout your app code, so today we'll be going through an easy way to refactor between controlled and uncontrolled components.

Note: In this article, we'll start with a controlled component and refactor our way to being uncontrolled. But the same logic applies in the reverse case -- you might find you often work from uncontrolled to controlled while you develop.

If you're not sure about what uncontrolled and controlled components are, or need a refresher, I recommend React Training's lectures. From what I can tell they are free to signup for, and the last lecture in particular is a great explanation. As Ryan Florence says (I'm paraphrasing) - once you start thinking in terms of control, your understanding of React really takes off.

Let's start by looking at this component:

function Component1({ children, activeIndex }) {
return children[activeIndex];
}

This component is completely controlled, it relies on the activeIndex prop to update which child it renders. The primary use case for this kind of thing is when you have multiple components that rely on the same activeIndex value and you want them all to stay in sync.

Now suppose your component wants to be able to update the activeIndex value with some logic it handles internally. A good example of this would be a button that takes the user to the next screen. You'll need an onChange prop:

function Component2({ children, activeIndex, onChange }) {
return (
<div>
{children[activeIndex]}
<button onClick={() => onChange(activeIndex + 1)}>Increment<button>
</div>
)
}

This is a pretty standard example of what a controlled component needs - a value and an updater function for that value.

Suppose you finished implementing your component and it's working well in this controlled state. There's a new feature that is coming along, and you want to capture the same functionality you implemented here, but it doesn't need to share its activeIndex value at all. In fact, you want all of this state to be managed inside of Component and not have to worry about it. Let's reimplement to see what this looks like:

function Component3({ children, initialIndex = 0 }) {
const [activeIndex, onChange] = useState(initialIndex)

return (
<div>
{children[activeIndex]}
<button onClick={() => onChange(activeIndex + 1)}>Increment<button>
</div>
)
}

You can see that the return statement is identical, but the value and updater function are setup inside the component instead. It would be nice if we could capture both of these states in our implementation and reuse the same component across multiple use cases, so let's do that:

function Component({
children,
activeIndex: parentActiveIndex,
onChange: parentOnChange,
initialIndex = 0 }
) {

const [_activeIndex, _onChange] = useState(initialIndex)
const isControlled = parentActiveIndex !== undefined && parentOnChange !== undefined

const activeIndex = isControlled ? parentActiveIndex : _activeIndex
const onChange = isControlled ? parentOnChange : _onChange

return (
<div>
{children[activeIndex]}
<button onClick={() => onChange(activeIndex + 1)}>Increment<button>
</div>
)
}

What's shared between our two implementations is the use of the activeIndex and onChange, so all we need to do is figure out which one to use based on the props our component receives. You'll notice we didn't change anything in the return statement of our component, just added some logic above it to make our component more flexible.

This pattern can be applied to any set of controlled props - Cheers!