March 29, 2019
React Hooks have been out for a couple of months now, and developers are already seeing how they simplify state management in functional components. One common use case for state is handling form inputs, which traditionally required a lot of boilerplate code in class components.
With useState and useReducer, we can now manage form state in a much cleaner way. In this post, we’ll explore:
- Controlled vs. uncontrolled components in forms
- When to use
useStatevs.useReducer - How to manage complex forms efficiently
Controlled vs. Uncontrolled Components
In React, form elements can be controlled or uncontrolled:
- Controlled components – React fully manages the input’s state via
useState(oruseReducer). - Uncontrolled components – The input’s state is handled by the DOM itself, accessed via
ref.
In most cases, controlled components are the preferred approach because they allow React to manage form state predictably.
Example: Controlled Component with useState
Before Hooks, we used this.state to track form input values in class components:
class Form extends React.Component {
constructor(props) {
super(props);
this.state = { name: "" };
}
handleChange = (event) => {
this.setState({ name: event.target.value });
};
render() {
return (
<input
type="text"
value={this.state.name}
onChange={this.handleChange}
/>
);
}
}
Now, with Hooks, we can use useState to manage form inputs in a much cleaner way:
import React, { useState } from "react";
function Form() {
const [name, setName] = useState("");
return (
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
);
}
Why is this better?
- No need for a class or constructor.
- No need to bind
this. - Code is shorter and easier to read.
Using useReducer for Complex Forms
For simple forms, useState works fine. But what if you have multiple fields or need more advanced state logic (e.g., resetting, validation, handling multiple actions)? This is where useReducer shines.
Example: Managing a Multi-Field Form with useReducer
Instead of managing multiple useState calls, we can use useReducer to centralize state updates.
Step 1: Define the Reducer Function
function formReducer(state, action) {
switch (action.type) {
case "CHANGE":
return { ...state, [action.field]: action.value };
case "RESET":
return { name: "", email: "" };
default:
return state;
}
}
Step 2: Use useReducer in the Component
import React, { useReducer } from "react";
function Form() {
const [state, dispatch] = useReducer(formReducer, { name: "", email: "" });
return (
<form>
<input
type="text"
value={state.name}
onChange={(e) =>
dispatch({ type: "CHANGE", field: "name", value: e.target.value })
}
/>
<input
type="email"
value={state.email}
onChange={(e) =>
dispatch({ type: "CHANGE", field: "email", value: e.target.value })
}
/>
<button type="button" onClick={() => dispatch({ type: "RESET" })}>
Reset
</button>
</form>
);
}
Why useReducer?
- Centralized state logic – Instead of multiple
useStatecalls, all updates go through the reducer. - Scalability – Adding new fields or actions is easy.
- Cleaner code – Especially useful when handling complex form behavior.
When to Use useState vs. useReducer?
| Scenario | Use useState | Use useReducer |
|---|---|---|
| Simple forms with a few fields | ✅ | ❌ |
| Forms with multiple fields | ⚠️ (can get messy) | ✅ |
| Complex validation and logic | ❌ | ✅ |
| Multiple state transitions (reset, conditional updates) | ❌ | ✅ |
General rule: If your form is simple, stick with useState. If it gets more complex, switch to useReducer to keep things organized.
Conclusion
React Hooks have made form management in functional components much easier. While useState is great for simple forms, useReducer provides better control for handling more complex form logic.
If you’re new to Hooks, now is the perfect time to start integrating them into your workflow. With React moving towards a functional-first approach, mastering Hooks will be essential in 2019 and beyond.