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
useState
vs.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
useState
calls, 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.