Uncontrolled vs Controlled Components in React: Components and Form Elements
📝Introduction
Recently, I came across an interesting topic in the React documentation about uncontrolled and controlled components, especially regarding how components are derived through states and props.
Initial confusion: I used to think these terms solely applied to form elements. For instance, a text input field is controlled through React state (Controlled) vs directly through DOM references without React's control (Uncontrolled). However, after diving deeper, I realized that "controlled" and "uncontrolled" have specific meanings in the context of both React components and form elements.
Key point: Understanding which component controls the state in react components was a key to clear my confusion.
Let’s deep dive into what I've learnt after coming out from my initial confusion.
🤔 Why it matters?
It is essential to understand it because this will help in managing states efficiently when states start growing and lead to creating more flexible and maintainable components (easy to change). I believe that this will also clear some understanding in dealing with forms elements vs react components. I often find that the terms "uncontrolled" and "controlled" components in the context of "React components" are mostly used in creating modular and reusable UI components.
🤷♂️ What are the differences?
Let’s deep dive into the difference between Uncontrolled and controlled components in different contexts. Here the two contexts are React components and Form elements.
React components context
Controlled Components
A controlled component is one where the important information in the component is derived from the parent component via props.
The parent component controls the state and behaviour of the child component.
Example: An expandable block component where the
isActive
state is managed by the parent component.
// ExpandableBlock.ts
interface ControlledExpandableBlockProps {
isActive: boolean;
onToggle: () => void;
}
function ControlledExpandableBlock(
props: React.PropsWithChildren<ControlledExpandableBlockProps>,
) {
return (
<div>
<button onClick={props.onToggle}>
{props.isActive ? "Collapse" : "Expand"}
</button>
{props.isActive && props.children}
</div>
);
}
Uncontrolled Component:
An uncontrolled component manages its state by itself. The child component makes important decisions on its own without relying on the parent component.
Example:
UncontrolledExpandableBlock
manages theisActive
(important state) to control the visibility of child nodes. Usually, in this type of component, the state is not bothered by the parent and doesn't care what value it holds.
// UncontrolledExpandableBlock.tsx
function UncontrolledExpandableBlock({
children,
}: React.PropsWithChildren<{}>) {
const [isActive, setIsActive] = useState(false);
const toggleBlock = () => {
setIsActive(!isActive);
};
return (
<div>
<button onClick={toggleBlock}>
{isActive ? "Collapse" : "Expand"}
</button>
{isActive && children}
</div>
);
}
Form Elements context
Controlled Form Element:
Form elements like text input fields are controlled through the React state.
The value of the form element is managed by the React state, and any changes to the input are handled by updating the state (using event handlers).
This ensures that the form data is always in sync with the component's state, providing a single source of truth.
Example: Controlled input takes control of the `value` state independently.
function ControlledInput() {
const [value, setValue] = useState('')
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value)
}
return <input type="text" value={value} onChange={handleChange} />
}
Uncontrolled Form Element:
Form elements are managed directly through DOM references without React's control.
The value is handled by the DOM itself, and you can access the current value using a ref.
This approach can be useful for integrating with third-party libraries like formik, and reack-hook-form or when you need to manage the form state outside of React.
function UncontrolledInput() {
const inputRef = useRef<HTMLInputElement>(null)
const handleSubmit = () => {
alert(inputRef.current?.value)
}
return (
<div>
<input type="text" ref={inputRef} />
<button onClick={handleSubmit}>Submit</button>
</div>
)
}
😵 The confusion
Controlled form elements are managed by states, so they are called controlled components. This is true for React components, where states are managed inside the component. However, it is easy to get mixed and call an uncontrolled component a controlled component because of the presence of form elements within the component. If a component has form elements and their states are managed by its parent component, should the component be called a controlled component?
🧹 Clearing the confusion
Mindset: There is no strict rule on what the component is called in this scenario. To remember this, I started to think that the components should be classified based on whether the parent components are controlling the behaviour of the children components or not. After that, I determine whether the components have controlled or uncontrolled form elements.
Let's understand this with an example. Consider a simple form with one input field: name. App
, will manage the state of these input fields and pass the state down to the child component, ControlledForm
, via props.
// ControlledForm.tsx
interface ControlledFormProps {
formData: { [key: string]: string }
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void
}
function ControlledForm({ formData, onChange }: ControlledFormProps) {
return (
<form>
<label>
Name:
<input
type="text"
name="name"
value={formData.name}
onChange={onChange}
/>
</label>
</form>
)
}
ControlledForm
is a reusable controlled component where the formData
state from the parent component has been passed to control the values of the form.
// App.tsx
function App() {
const [formData, setFormData] = useState({ name: '', email: '' })
const handleFormChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target
setFormData((prevData) => ({ ...prevData, [name]: value }))
}
// add handleSubmit
return (
<div>
<ControlledForm formData={formData} onChange={handleFormChange} />
</div>
)
}
App
component is controlling the states for ControlledForm
component. This design pattern is called an Inversion of Control (IoC) which means that the control of the component is inverted, consumers of the reusable component will control its behaviour. By leveraging IoC, you can create components that are more modular and easier to maintain.
🌍 In reality
In practice, components often have a mix of both states and props, calling a component to controlled or uncontrolled component would rarely happen. But, this is a useful way to talk about how components are designed and what capabilities they offer.
🔍 Further Exploration
To solidify your understanding, try identifying controlled and uncontrolled components within your projects. Experiment with the refactoring of components that you have in your refactoring dept list to explore the pros and cons of this design pattern.
I hope this explanation provides a clearer and more comprehensive understanding of controlled and uncontrolled components in React! 🤞
🎯 Conclusion
It is often crucial to remember who is controlling the states of your component. Is it your parent component or the component itself?
Once you get to know, everything about states will start making a lot of sense and you will be better at managing states.
And, then start considering whether the form elements are controlled via states or dom reference.
It is recommended that form elements are always better to be controlled. They can be called "controlled input" instead of calling the component a "controlled component".
Controlled components are usually created to make components more modular and reusable and can be shared across the application.
🌊Go Deeper
To learn more about this, check out some helpful resources that I came across along the way in understanding this concept.