Skip to content

Instantly share code, notes, and snippets.

@ebinion
Created October 31, 2023 21:17
Show Gist options
  • Save ebinion/e841da2397ef18cd9169fd0760f11203 to your computer and use it in GitHub Desktop.
Save ebinion/e841da2397ef18cd9169fd0760f11203 to your computer and use it in GitHub Desktop.

React "controlled components" can be detremental to the overall user experience when used improperly. For design systems work, we've elected to ensure our components are uncontrolled to reduce unnecessary rerenders and avoid the potential for UI jank. But what are controlled components?

React's documentation (https://react.dev/learn/sharing-state-between-components#controlled-and-uncontrolled-components) provides a definition of controlled components...

... a component is “controlled” when the important information in it is driven by props rather than its own local state.

While I appreciate the shortness and accuracy of the definition, I find it unhelpful in practical everyday work.

In practice, controlled components are components that use a combination of props and state to manage updates to the component. React does a really good job of managing rerendering of components by monitoring state and prop updates. That's particularily true when state flows unidirectionally. However controlled components can create unintentional loops in the flow of data, causing renders more expensive and increase the frequency of them.

In no component is this more observable then controlled input components.

// Controlled input
function TextInput({value: incomingValue, ...rest}) {
  const [value, setValue] = useState(incomingValue)
  
  // update the state as the user types
  const handleChange = (event) => {
    setValue(event.currentTarget.value)
  }

  // update the internal state if the parent component updates the prop
  useEffect(() => {
    setValue(incomingValue)
  }, [incomingValue])

  return <input type="text" onChange={handleChange} value={value} {...rest} />
}

Inside of the component there is a circular flow of data between the state and input component. In ideal circumstances, there's no competing processes operating on the user's main thread and the system has plenty of overhead, the user would be able to interact with the input as nornmal. However, those conditions are rarely true in web UIs. Often the app is requesting of sending data, running regex to validate input, auto-saving, etc. This leads to a slow down on renders and lagging state updates.

To make the same component "uncontrolled" it's as simple as removing the internal loop created to set and monitor values.

// Uncontrolled input
function TextInput(props) {
  return <input type="text" {...props} />
}

Uncontrolled components typically do not implement their own internal state to manage updates. This makes it easier to ensure unidirectional data flow and saves state updates for when they are truly needed. If a developer needed the value, of the input element the parent component could retrieve it by using the forwarded ref (not shown in our examples for simplicity's sake) or any of the provided event handlers.

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