5 Best Practices for Handling State Structure in React
Five best practices that can help to structure the state well

When we write a component in React that holds some state, we will have to make choices about how many state variables we want, and how to structure it.
Although it’s possible to write working code even with a standard state structure, here are a few steps to help you make better choices.
1. Group Related State
If two state variables change together, it is a good practice to combine them into a single state variable.
Example:
Unify the two variables:
const [x, setX] = useState(0);
const [y, setY] = useState(0);
Into the below structure:
const [position, setPosition] = useState({ x: 0, y: 0 });
Technically, both have the same approach. but it lessens the lines of code.
Note: If your state is an object, you cannot update only one field in it, hence you can do something like this setPosition({...position, x:100})
.
2. Avoid Duplicate in State
When the same data is duplicated between multiple state variables or within nested objects, it is difficult to keep in sync
Example:
import { useState } from 'react';const initialItems = [
{ id: 0, name: 'Indian Cuisine', },
{ id: 1, name: 'Italian Cuisine', },
{ id: 2, name: 'Chinese Cuisine' },
];export default function Menu() {
const [items, setItems] = useState(initialItems);
const [selectedItem, setSelectedItem] = useState(
items[0]
);return (
<>
<h2>What's your Favourite Cuisine?</h2>
<ul>
{items.map(item => (
<li key={item.id}>
{item.name}
{' '}
<button onClick={() => {
setSelectedItem(item);
}}>Choose</button>
</li>
))}
</ul>
<p>You picked {selectedItem.name}.</p>
</>
);
}
Currently, it stores the selected item as an object in the selectedItem
state variable. However, this is not great: the contents of the selectedItem
is the same object as one of the items inside the items
list. This means that the information about the item itself is duplicated in two places.
Although you could update selectedItem
too, an easier fix is to remove duplication. In below this example, instead of a selectedItem
object (which creates a duplication with objects inside items
), you hold the selectedId
in state, and then get the selectedItem
by searching the items
array for an item with that ID .
Example:
import { useState } from 'react';const initialItems = [
{ id: 0, name: 'Indian Cuisine', },
{ id: 1, name: 'Italian Cuisine', },
{ id: 2, name: 'Chinese Cuisine' },
];export default function Menu() {
const [items, setItems] = useState(initialItems);
const [selectedId, setSelectedId] = useState(0);const selectedItem = items.find(item =>
item.id === selectedId
);function handleItemChange(id, e) {
setItems(items.map(item => {
if (item.id === id) {
return {
...item,
name: e.target.value,
};
} else {
return item;
}
}));
}return (
<>
<h2>What's your travel snack?</h2>
<ul>
{items.map((item, index) => (
<li key={item.id}>
<input
value={item.name}
onChange={e => {
handleItemChange(item.id, e)
}}
/>
{' '}
<button onClick={() => {
setSelectedId(item.id);
}}>Choose</button>
</li>
))}
</ul>
<p>You picked {selectedItem.name}.</p>
</>
);
}
You don’t need to store the selected item in state, because only the selected ID is essential.
3. Avoid Redundant State
If you can calculate some information from the component’s props or its existing state variables during rendering, you should not put that information into that component’s state.
Example:
import { useState } from 'react';export default function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');function handleFirstNameChange(e) {
setFirstName(e.target.value);
setFullName(e.target.value + ' ' + lastName);
}function handleLastNameChange(e) {
setLastName(e.target.value);
setFullName(firstName + ' ' + e.target.value);
}return (
<>
<h2>Let’s check you in</h2>
<label>
First name:{' '}
<input
value={firstName}
onChange={handleFirstNameChange}
/>
</label>
<label>
Last name:{' '}
<input
value={lastName}
onChange={handleLastNameChange}
/>
</label>
<p>
Your ticket will be issued to: <b>{fullName}</b>
</p>
</>
);
}
This form has three state variables: firstName
, lastName
, and fullName
. However, fullName
is redundant. You can always calculate fullName
from firstName
and lastName
during render, so remove it from state.
This is how you can do it:
import { useState } from 'react';export default function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');const fullName = firstName + ' ' + lastName;function handleFirstNameChange(e) {
setFirstName(e.target.value);
}function handleLastNameChange(e) {
setLastName(e.target.value);
}return (
<>
<h2>Let’s check you in</h2>
<label>
First name:{' '}
<input
value={firstName}
onChange={handleFirstNameChange}
/>
</label>
<label>
Last name:{' '}
<input
value={lastName}
onChange={handleLastNameChange}
/>
</label>
<p>
Your ticket will be issued to: <b>{fullName}</b>
</p>
</>
);
}
4. Avoid Contradiction in State
When the state is structured in a way that several pieces of state may contradict, you probably have a chance to make mistakes. Try to avoid this.
Example:
import { useState } from 'react';export default function FeedbackForm() {
const [text, setText] = useState('');
const [isSending, setIsSending] = useState(false);
const [isSent, setIsSent] = useState(false);async function handleSubmit(e) {
e.preventDefault();
setIsSending(true);
await sendMessage(text);
setIsSending(false);
setIsSent(true);
}if (isSent) {
return <h1>Thanks for feedback!</h1>
}return (
<form onSubmit={handleSubmit}>
<p>How was your stay at The Prancing Pony?</p>
<textarea
disabled={isSending}
value={text}
onChange={e => setText(e.target.value)}
/>
<br />
<button
disabled={isSending}
type="submit"
>
Send
</button>
{isSending && <p>Sending...</p>}
</form>
);
}// Pretend to send a message.
function sendMessage(text) {
return new Promise(resolve => {
setTimeout(resolve, 2000);
});
}
While this code works, it leaves you in confusion of the what state to update exactly. For example, if you forget to call setIsSent
and setIsSending
together, you may end up in a situation where both isSending
and isSent
are true
at the same time.
Since isSending
and isSent
should never be true
at the same time, it is better to replace them with one status
state variable that may take one of three valid states: 'typing'
(initial), 'sending'
, and 'sent'
import { useState } from 'react';export default function FeedbackForm() {
const [text, setText] = useState('');
const [status, setStatus] = useState('typing');async function handleSubmit(e) {
e.preventDefault();
setStatus('sending');
await sendMessage(text);
setStatus('sent');
}const isSending = status === 'sending';
const isSent = status === 'sent';if (isSent) {
return <h1>Thanks for feedback!</h1>
}return (
<form onSubmit={handleSubmit}>
<p>How was your stay at The Prancing Pony?</p>
<textarea
disabled={isSending}
value={text}
onChange={e => setText(e.target.value)}
/>
<br />
<button
disabled={isSending}
type="submit"
>
Send
</button>
{isSending && <p>Sending...</p>}
</form>
);
}// Pretend to send a message.
function sendMessage(text) {
return new Promise(resolve => {
setTimeout(resolve, 2000);
});
}
The more complex your component is, the harder it will be to understand what happened.
5. Avoid Deeply Nested State
Deeply hierarchical state is not very easy to update, hence it is a best practice to make the state structure flat.
Example:
import { useState } from 'react';const initialTravelPlan = {
id: 0,
title: '(Root)',
childPlaces: [{
id: 1,
title: 'Earth',
childPlaces: [{
id: 2,
title: 'Africa',
childPlaces: [{
id: 3,
title: 'Botswana',
childPlaces: []
}, {
id: 4,
title: 'Egypt',
childPlaces: []
}, {
id: 5,
title: 'Kenya',
childPlaces: []
}, {
id: 6,
title: 'Madagascar',
childPlaces: []
}, {
id: 7,
title: 'Morocco',
childPlaces: []
}, {
id: 8,
title: 'Nigeria',
childPlaces: []
}, {
id: 9,
title: 'South Africa',
childPlaces: []
}]
}, {
id: 10,
title: 'Asia',
childPlaces: [{
id: 11,
title: 'China',
childPlaces: []
}, {
id: 12,
title: 'Hong Kong',
childPlaces: []
}, {
id: 13,
title: 'India',
childPlaces: []
}, {
id: 14,
title: 'Singapore',
childPlaces: []
}, {
id: 15,
title: 'South Korea',
childPlaces: []
}, {
id: 16,
title: 'Thailand',
childPlaces: []
}, {
id: 17,
title: 'Vietnam',
childPlaces: []
}]
},{
id: 18,
title: 'Mars',
childPlaces: [{
id: 19,
title: 'Corn Town',
childPlaces: []
}, {
id: 20,
title: 'Green Hill',
childPlaces: []
}]
}]
};function PlaceTree({ id, placesById }) {
const place = placesById[id];
const childIds = place.childIds;
return (
<>
<li>{place.title}</li>
{childIds.length > 0 && (
<ol>
{childIds.map(childId => (
<PlaceTree
key={childId}
id={childId}
placesById={placesById}
/>
))}
</ol>
)}
</>
);
}export default function TravelPlan() {
const [plan, setPlan] = useState(initialTravelPlan);
const root = plan[0];
const planetIds = root.childIds;
return (
<>
<h2>Places to visit</h2>
<ol>
{planetIds.map(id => (
<PlaceTree
key={id}
id={id}
placesById={plan}
/>
))}
</ol>
</>
);
}
You can nest state as much as you like, but making it “flat” can solve numerous problems. It makes state easier to update, and it helps ensure you don’t have duplication in different parts of a nested object.
Conclusion
Choosing the state structure is very essential to avoid confusions and duplications. Taking care of minor things while writing the code leads to perfect optimized code.
Build composable web applications
Don’t build web monoliths. Use Bit to create and compose decoupled software components — in your favorite frameworks like React or Node. Build scalable frontends and backends with a powerful and enjoyable dev experience.
Bring your team to Bit Cloud to host and collaborate on components together, and greatly speed up, scale, and standardize development as a team. Start with composable frontends like a Design System or Micro Frontends, or explore the composable backend. Give it a try →
