Multiple State Variables vs. a Single State Object #56
π Introductionβ
After mastering the intricacies of the functional update pattern, a new question arises: how should we structure our state? When a component needs to keep track of multiple pieces of dataβlike several form inputsβshould you use multiple, independent useState
calls, or group them all into a single object within one useState
call?
This is a common architectural decision in React. In this article, we'll explore the pros and cons of each approach and establish clear principles to help you decide which pattern is best for your specific use case.
π Prerequisitesβ
Before we begin, please ensure you have a solid grasp of the following concepts:
- The
useState
hook. - How to update objects in state immutably using the spread (
...
) syntax.
π― Article Outline: What You'll Masterβ
In this article, you will learn:
- β
Two Core Approaches: A side-by-side comparison of managing state with multiple
useState
calls versus a single state object. - β Principle 1: Grouping Related State: Understanding when to group state variables that change together.
- β Principle 2: Avoiding Contradictions: How separate state variables can sometimes lead to "impossible" states and how to prevent this.
- β Practical Application: Refactoring a form component from one pattern to the other to see the trade-offs in action.
- β
Best Practices: Clear guidelines on when to prefer multiple state variables and when an object or a
useReducer
hook might be a better fit.
π§ Section 1: The Two Approaches Side-by-Sideβ
Let's consider a simple login form with username
and password
fields. We can manage its state in two ways.
Approach 1: Multiple useState
Callsβ
This is often the most straightforward approach, especially for simple components. Each piece of state gets its own "slot".
// code-block-1.jsx
import React, { useState } from 'react';
function LoginFormMultipleStates() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
// ... form JSX and event handlers ...
}
- Pros: Very easy to read and understand. Updating one piece of state is simple and doesn't affect the others (e.g.,
setUsername(newValue)
). - Cons: Can become cluttered if you have many (e.g., 5+) state variables.
Approach 2: A Single State Objectβ
This approach feels more like the traditional this.state
from class components, where all state lived in a single object.
// code-block-2.jsx
import React, { useState } from 'react';
function LoginFormSingleObject() {
const [formState, setFormState] = useState({
username: '',
password: '',
});
// ... form JSX and event handlers ...
}
- Pros: Keeps related data neatly grouped together. Can be easier to pass all form data around as a single variable.
- Cons: Updates are more verbose. You must always remember to spread the previous state (
...prevState
) to avoid losing data, as theuseState
setter replaces the state, it doesn't merge it.
π» Section 2: Guiding Principles for Structuring Stateβ
So, which approach is better? The official React documentation provides excellent principles to guide this decision. The main takeaway is: it depends on how the state changes.
Principle 1: Group Related Stateβ
If you almost always update two or more state variables at the same time, consider merging them into a single state variable.
Imagine tracking a cursor's position. The x
and y
coordinates are rarely updated independently. They almost always change together.
Good: Grouped State
// code-block-3.jsx
const [position, setPosition] = useState({ x: 0, y: 0 });
function handlePointerMove(e) {
setPosition({
x: e.clientX,
y: e.clientY
});
}
This is clean and makes logical sense. The position
is a single concept.
Less Ideal: Separate States
// code-block-4.jsx
const [x, setX] = useState(0);
const [y, setY] = useState(0);
function handlePointerMove(e) {
setX(e.clientX);
setY(e.clientY);
}
This works, but it's slightly more verbose and treats x
and y
as independent, when they are conceptually linked.
Principle 2: Avoid Contradictions in Stateβ
When your state structure allows for "impossible" combinations, you leave room for bugs. Try to model your state so that contradictions are not possible.
Consider a data-fetching component. You might be tempted to use two booleans: isLoading
and isError
.
Bad: Contradictory State
// code-block-5.jsx
const [isLoading, setIsLoading] = useState(true);
const [isError, setIsError] = useState(false);
// What does it mean if both are true? This state is possible but shouldn't be.
What happens if a bug causes both isLoading
and isError
to be true
? Your UI could show both a loading spinner and an error message. This is an "impossible" state that your structure allows.
Good: A Single status
Variable
A better approach is to use a single status
variable that can only be in one state at a time.
// code-block-6.jsx
const [status, setStatus] = useState('loading'); // e.g., 'loading', 'success', 'error'
// You can still derive booleans for convenience without putting them in state:
const isLoading = status === 'loading';
const isError = status === 'error';
With this structure, it's impossible for the component to be in both a loading and an error state simultaneously. You've made the "impossible" states impossible.
π οΈ Section 3: Practical Example - A User Settings Formβ
Let's build a settings form and compare the two approaches.
Version 1: Multiple useState
callsβ
// project-example-multiple.jsx
import React, { useState } from 'react';
function SettingsFormMultiple() {
const [username, setUsername] = useState('guest');
const [theme, setTheme] = useState('light');
const [notifications, setNotifications] = useState(true);
// Handlers are simple and direct
const handleUsernameChange = (e) => setUsername(e.target.value);
const handleThemeChange = (e) => setTheme(e.target.value);
const handleNotificationsChange = (e) => setNotifications(e.target.checked);
return (
// ... JSX for the form ...
);
}
This is very clear and easy to follow. Each piece of state is independent.
Version 2: Single State Objectβ
// project-example-single.jsx
import React, { useState } from 'react';
function SettingsFormSingle() {
const [settings, setSettings] = useState({
username: 'guest',
theme: 'light',
notifications: true,
});
// A single, generic handler can update any field
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
setSettings(prevSettings => ({
...prevSettings,
[name]: type === 'checkbox' ? checked : value,
}));
};
return (
// ... JSX for the form, using `settings.username`, etc.
// and passing `handleChange` to all inputs.
);
}
This version is more concise in its event handling but requires more care inside the handleChange
function to correctly spread the previous state and handle different input types.
π‘ Conclusion & Key Takeawaysβ
There is no single "right" answer, but the community and official docs provide strong guidance.
Let's summarize the key takeaways:
- Prefer Multiple States for Unrelated Data: If state variables don't change together, using multiple
useState
calls is often cleaner and less error-prone. This is the most common and recommended approach. - Group State for Related Data: If variables always change at the same time (like
x
andy
coordinates), grouping them in an object makes sense. - Use a Single
status
for State Machines: For states that cannot and should not overlap (likeloading
,success
,error
), use a single string or enum variable to make impossible states impossible. - Consider
useReducer
for Complexity: If you find yourself updating multiple pieces of state in a single event handler, or if the update logic is complex, it's a strong signal that you should consider graduating to auseReducer
hook, which we will cover in a later chapter.
Challenge Yourself:
Take the SettingsFormSingle
example. Add a "Reset to Defaults" button that resets the entire settings
object back to its initial state in a single click.
β‘οΈ Next Stepsβ
This article concludes our deep dive into the useState
hook. You now have a robust toolkit for managing local component state. In the next series, "Sharing Data Between Components", we will tackle a new challenge: what to do when different components need to access and manipulate the same piece of state.
Thank you for your dedication. Stay curious, and happy coding!
glossaryβ
- State Colocation: The practice of keeping state as close as possible to the components that use it.
- State Machine: A model of computation where a system can be in one of a finite number of states at any given time. Using a single
status
variable is a simple form of a state machine.