Cover Image for Uncontrolled Chaos: Why React Devs Still Struggle with Form Components

Uncontrolled Chaos: Why React Devs Still Struggle with Form Components

Bartek Witczak
Bartek Witczak

I've read & reread React documentation multiple times. Quite a lot of things are explained really well. The truth is that when I was starting working with React in late 2014 there was almost no other source apart from documentation. And maybe that helped me to master React from the ground up? 🤔 Now we've got gazillion blogs about everything. But there's 1 tiny problem. Devs still don't get the basics. Devs still don't get what is a difference between controlled & uncontrolled components. Let's tackle that one (so that we have another blog post on React - number gazillion + 1). And later let's move to the reason why that's still the case.

Let's start with first principle of React. React state should be the source of truth. Period. DOM is direct representation of React state. Devs manipulate state of React components and as a result we got changes in DOM. That's the premise that got complex (& simple) projects to React. Simplicity. Before 2014 projects were (usually) manipulating DOM directly. Bugs were hard to debug. UI state was a result of many manipulations and it was hard to understand how did we get to that state. Maybe there were some cascading changes? Maybe we forgot to change few places? Maybe we added too many classes? Maybe we forgot to remove some classes? Maybe we added some elements, but forgot to cleanup? …

React is function of state

We can describe React as a function of current state that returns DOM state. (That's why React devs often like & are interested in functional programming. The premise is that DOM state is pure function of React state.)

2 types of "form" components

There are 2 types of components. To make it clear once and for all.

  • controlled - the source of truth is in React state
  • uncontrolled - the source of truth is in DOM (Now you can see why I'm biased toward controlled component. They are aligned with React first principle.)
Types of components

Why do devs still use uncontrolled component?

My first argument is that they don't know two types of components exist or they don't understand them. That's pretty easy that if you don't know something exists, you won't use it.

But let's put that argument aside for a moment. Let's get back to documentation. If you take a look at legacy documentation (Uncontrolled Components), it's pretty clear that controlled components are the way to go.

In most cases, we recommend using controlled components to implement forms. In a controlled component, form data is handled by a React component. The alternative is uncontrolled components, where form data is handled by the DOM itself.

The new version of docs is not that straightforward. First there's no longer a page about uncontrolled components. It's moved to <input/> page. Then somewhere in the middle we've Controlling an input with a state variable section:

An input like <input /> is uncontrolled. Even if you pass an initial value like <input defaultValue="Initial text" />, your JSX only specifies the initial value. It does not control what the value should be right now.

To render a controlled input, pass the value prop to it (or checked for checkboxes and radios). React will force the input to always have the value you passed.

Interesting. Now if you're getting started with React & want to use <input />, I guess you're going to start with uncontrolled version. First few examples show uncontrolled component with fetching values using form data. Now it's not that obvious which one to use. What's more, another paragraph doesn't make things easier:

A controlled input makes sense if you needed state anyway—for example, to re-render your UI on every edit.

Now you think: I DON'T WANT TO RE-RENDER MY UI ON EVERY KEYSTROKE 😱 But the thing is React is going to "re-render" only from the place where state changes and down. Usually input is at the bottom (or close to bottom) of components tree. Performance hit doesn't make difference!!! Unless state re-renders a large tree, it doesn't make a difference. If it is re-rendering a large tree (like you've got input really high in tree structure), there are ways to optimize it. Take a look at useDefferedValue.

So why do we have uncontrolled components?

Performance. Performance. Performance. Of course there's a performance hit. If and only if performance is your top priority, uncontrolled components are solution. https://github.com/react-hook-form/react-hook-form is a great example of library that's relies on uncontrolled components (Performance of React Hook Form). Performance was the reason. If you need performant solution in your project, I suggest using react-hook-form. Managing uncontrolled inputs by yourself is hard.

Why uncontrolled components sounds like a problem?

So you might ask: Bartek, so what's the problem with uncontrolled components? Ofc we need the best performance. I'd say that usually performance is not your top problem. The speed of delivering new features is a problem. Bugs are your main problem. Lack of quality is a problem. Regressions are a problem. Collaborating with team members is a problem. Performance is a good problem to have. Often when you have a performance problem & that's your biggest problem, you're in a good spot. I bet you have product-market fit by this time & your business is generating money.

To be honest I remember only a single case when performance was a key issue. It was a very big, multi-step form in the traveling domain. The form was using redux-form. Literally everything was kept in Redux. EVERYTHING. 🤯 You could spot the performance issue. That was one of the first things to resolve in the project.

Uncontrolled components bring problems to the table. As a React dev you're used to keeping the source of truth in React state. Uncontrolled component is just counter-intuitive. Thus it often starts as an uncontrolled component, but then you need to add additional behavior. You need state to display some data. You need state to clean the form. Some devs keep up with uncontrolled components and they start hacking. They use refs. They fetch to DOM to get data. DOM starts to be the source of truth. You didn't even notice and you're back in jQuery times. 😬

Always start with controlled components!

Never say never, and never say always. Controlled components should be the default mode. Start with a controlled component and if really needed move to uncontrolled. Controlled components provide a more straightforward, declarative way of managing form state in React. I assume you'd save a lot of time on debugging.

Take time to learn fundamentals

We all talk about productivity. We lack time to get deep enough to understand. People lack fundamentals. That's why I still meet people who don't understand controlled & uncontrolled components. During presentations, I often ask how many people read React docs back-to-back. It's less than 5% who raise their hand. Most projects have really good docs. Teams understand that good docs === good DX (developer experience). We go straight to blogs, videos, shorts. Now we also ask LLM. And that's fine. Just add a question about fundamentals. 😎

Slow is smooth. Smooth is fast.

Take time to learn fundamentals. You're going to be faster in the long run.

What are my takeaways?

Favor simplicity: When building forms in React, start with the simplest approach that could possibly work. In most cases, this means using controlled components, as they provide a clear, declarative way of managing form state.

Introduce uncontrolled components judiciously: If you have a compelling performance reason to use uncontrolled components, make sure you fully understand the tradeoffs and implications. Clearly document your decision and the specific scenarios where uncontrolled components are used.

Focus on delivering value: Remember that the ultimate goal is to deliver a high-quality, performant application that meets the needs of your users. Don't get too caught up in dogma or perfectionism, and be pragmatic in your decision-making.

Educate your team: Make sure all developers working on the project understand the differences between controlled and uncontrolled components, and the rationale behind your chosen approach. Share knowledge and best practices to maintain a consistent, maintainable form architecture.