Controlled vs. Uncontrolled Components in React
React is a powerful library for building user interfaces, and one of its core strengths lies in its flexibility. Among the many design patterns React offers, controlled and uncontrolled components are fundamental. Understanding these patterns can significantly impact how you manage state and handle user input in your applications.
Uncontrolled Components
Uncontrolled components are those where the component itself maintains its own internal state. The only time you interact with this state is when an event occurs, such as a form submission. For instance, consider a form where you only access the input values when the user hits the submit button. Until that event, the form's state is entirely managed by the DOM elements themselves.
Here's a simple example of an uncontrolled form:
import React, { createRef } from 'react'; export const UncontrolledForm = () => { const nameInput = createRef(); const ageInput = createRef(); const hairColorInput = createRef(); const handleSubmit = (e) => { e.preventDefault(); console.log(nameInput.current.value); console.log(ageInput.current.value); console.log(hairColorInput.current.value); }; return ( <form onSubmit={handleSubmit}> <input name="name" type="text" placeholder="Name" ref={nameInput} /> <input name="age" type="number" placeholder="Age" ref={ageInput} /> <input name="hairColor" type="text" placeholder="Hair Color" ref={hairColorInput} /> <input type="submit" value="Submit" /> </form> ); };
In this example, the form's state is only accessed when the form is submitted. The createRef
function is used to create references to the input elements, which are then accessed in the handleSubmit
function.
Controlled Components
Controlled components, on the other hand, rely on their parent component to manage their state. The state is passed down as props, and any changes to the state are handled by callback functions provided by the parent.
Here's how you might implement a controlled form:
import React, { useState } from 'react'; export const ControlledForm = () => { const [name, setName] = useState(''); const [age, setAge] = useState(''); const [hairColor, setHairColor] = useState(''); const handleSubmit = (e) => { e.preventDefault(); console.log(name, age, hairColor); }; return ( <form onSubmit={handleSubmit}> <input name="name" type="text" placeholder="Name" value={name} onChange={(e) => setName(e.target.value)} /> <input name="age" type="number" placeholder="Age" value={age} onChange={(e) => setAge(Number(e.target.value))} /> <input name="hairColor" type="text" placeholder="Hair Color" value={hairColor} onChange={(e) => setHairColor(e.target.value)} /> <button type="submit">Submit</button> </form> ); };
In this controlled form, the state of each input is managed by React's useState
hook. The value
prop of each input is tied to its corresponding state variable, and the onChange
handler updates the state whenever the user types into the input.
Why Prefer Controlled Components?
Controlled components are generally preferred for several reasons:
- Reusability: Controlled components are more reusable because their behavior is determined by their props rather than their internal state.
- Testability: They are easier to test since you can set up a component with a specific state and verify its behavior without needing to simulate user interactions.
- Predictability: With controlled components, you have a single source of truth for your state, making your application more predictable and easier to debug.
Practical Examples
Uncontrolled Forms
Uncontrolled forms defer most of their logic to the DOM elements. Here's an example:
import React, { createRef } from 'react'; export const UncontrolledForm = () => { const nameInput = createRef(); const ageInput = createRef(); const hairColorInput = createRef(); const handleSubmit = (e) => { e.preventDefault(); console.log(nameInput.current.value); console.log(ageInput.current.value); console.log(hairColorInput.current.value); }; return ( <form onSubmit={handleSubmit}> <input name="name" type="text" placeholder="Name" ref={nameInput} /> <input name="age" type="number" placeholder="Age" ref={ageInput} /> <input name="hairColor" type="text" placeholder="Hair Color" ref={hairColorInput} /> <input type="submit" value="Submit" /> </form> ); };
Controlled Forms
Controlled forms manage their state through React's useState
hook:
import React, { useState } from 'react'; export const ControlledForm = () => { const [name, setName] = useState(''); const [age, setAge] = useState(''); const [hairColor, setHairColor] = useState(''); const handleSubmit = (e) => { e.preventDefault(); console.log(name, age, hairColor); }; return ( <form onSubmit={handleSubmit}> <input name="name" type="text" placeholder="Name" value={name} onChange={(e) => setName(e.target.value)} /> <input name="age" type="number" placeholder="Age" value={age} onChange={(e) => setAge(Number(e.target.value))} /> <input name="hairColor" type="text" placeholder="Hair Color" value={hairColor} onChange={(e) => setHairColor(e.target.value)} /> <button type="submit">Submit</button> </form> ); };
Controlled Modals
Modals can also be controlled or uncontrolled. An uncontrolled modal manages its own visibility state:
import React, { useState } from 'react'; export const UncontrolledModal = () => { const [shouldShow, setShouldShow] = useState(false); return ( <> <button onClick={() => setShouldShow(true)}>Show Modal</button> {shouldShow && ( <div className="modal"> <p>Modal Content</p> <button onClick={() => setShouldShow(false)}>Close</button> </div> )} </> ); };
A controlled modal relies on its parent component to manage its visibility:
import React from 'react'; export const ControlledModal = ({ shouldShow, onRequestClose }) => { if (!shouldShow) return null; return ( <div className="modal"> <p>Modal Content</p> <button onClick={onRequestClose}>Close</button> </div> ); };
In the parent component:
import React, { useState } from 'react'; import { ControlledModal } from './ControlledModal'; const App = () => { const [shouldShowModal, setShouldShowModal] = useState(false); return ( <> <button onClick={() => setShouldShowModal(!shouldShowModal)}> {shouldShowModal ? 'Hide Modal' : 'Show Modal'} </button> <ControlledModal shouldShow={shouldShowModal} onRequestClose={() => setShouldShowModal(false)} /> </> ); }; export default App;
Conclusion
Controlled and uncontrolled components each have their place in React development. While uncontrolled components can be simpler and quicker to implement for basic use cases, controlled components offer greater flexibility, reusability, and testability. Understanding when and how to use each pattern will make you a more effective React developer.