Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save butaud/eabacec62e4340f28a5138da714ff779 to your computer and use it in GitHub Desktop.
Save butaud/eabacec62e4340f28a5138da714ff779 to your computer and use it in GitHub Desktop.
Controlled/uncontrolled state in React components

Controlled/uncontrolled state in React components

I wanted to share some things that I've learned recently about the "controlled/uncontrolled state" patterns in React components, especially Fluent components. Not having a clear understanding of these things up front caused us some confusion in some recent work items for left rail and grid view.

Definition

I'm just taking the terminology that the React documentation uses to talk about form controls (controlled vs uncontrolled). Suppose you have a component that has multiple visual states. For simplicity, say it renders a checkbox that is either checked or unchecked. The checked state is uncontrolled if the component manages it internally. The checked state is controlled if it is passed in by a prop. Since a component can't change its own props, that means that the state then needs to be managed by a parent component.

Usage

So which one should you use? Well obviously if one was always better these wouldn't be two patterns, they would be one pattern and one antipattern. There are situations where either one is preferred. As a general rule, if the parent component needs to know about or modify the state, the controlled pattern is preferred. If the parent component does not care about the state, the uncontrolled pattern is usually simpler.

But let's look deeper into when you would use each and why.

Interacting with child state

Suppose you are using a component and need to know what its current state is. The classic example would be an input field on a form - when the user types a value in a text field and clicks a submit button, some ancestor of both the text field and submit button will need to handle the click event by doing something with the value.

Querying the text field to see what the value is when it's needed is not very good React style, because that requires the container to have a reference to the current instance of the child component. It can be done using ref, but managing refs is a whole can of worms that you want to avoid if possible.

Instead, the text field should expose a callback so that the container is always aware of the current value. But if the text field is uncontrolled, this is a problem, because now the "current value" is stored in two places at once, which is also bad React style and can introduce problems. So the text field's value needs to be controlled - that way, the state in the parent component is the single source of truth, and the text field just displays what the parent sends in the props.

This becomes more important if the state needs to change in response to something outside of the component itself. For example, suppose you have a settings checkbox that exposes a preference stored in the backend somewhere. If that preference is changed on another client, the client should update the component that is currently being displayed. In general, parents should only control child components by props passed in, not by imperative commands to toggle an internal state. This is what gives React the flexibility to re-render and guarantee that the resulting state will be the same. So the checkbox state needs to be controlled by the parent component which receives the subscription event.

Complex child state logic

In other cases, it makes more sense to leave the child component state uncontrolled. For example, the MenuButton component has logic to automatically act as a context menu for a specified element, popping open when the element is right-clicked, long-touched, or when a standard keyboard shortcut is used. And in most cases, the parent doesn't need to care about whether the MenuButton is open or not. So to avoid duplicating the trigger logic, it makes sense to leave the open state uncontrolled.

Pitfalls

Here are some of the things to watch out for regarding these two patterns.

Component Support

If you are using a component you didn't write, you need to find out whether it supports one or both patterns. If there's no prop which allows you to manage the state you're concerned about, then you are stuck with it being uncontrolled. If the component doesn't do anything to manage its own state, then you're going to have to control it in your own code.

It's important to figure this out when you are working with Fluent components because they are not always consistent and the docs don't do a great job of highlighting the difference.

For example, the MenuButton component, which is a popup/flyout style menu, has the props defaultOpen and open. defaultOpen is for the uncontrolled pattern - you tell the component the initial state, but it is responsible for handling whether it should be open or not afterwards based on the user's interactions. open is for the controlled pattern, where it is up to the parent to decide what the open state should be.

The Tree component, on the other hand, has an expanded prop for its TreeItem children which at first glance appears to be useful for the controlled pattern. In fact, it only controls the initial expanded state, and the component allows the user to expand/collapse items even when the parent prop doesn't change. So it only supports the uncontrolled pattern.

Mixing controlled / uncontrolled patterns

Something you should definitely avoid whenever possible is mixing the controlled and uncontrolled patterns in one usage of a component. If you are going to control a value, then you should never rely on the component's own logic for managing that value. Ideally this is impossible because the component should respect the prop whenever it is set, but there can be cases where a component is not implemented properly and allows changes to the state that a prop is supposed to be controlling. If this happens, you can run into hard-to-find bugs where re-rendering the component causes it to snap back to a previous state. Remember that every state should have a single source of truth within the component hierarchy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment