Skip to main content

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 the useState 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.

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 and y coordinates), grouping them in an object makes sense.
  • Use a Single status for State Machines: For states that cannot and should not overlap (like loading, 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 a useReducer 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.

Further Reading​