5 Best Practices for Handling State Structure in React

Five best practices that can help to structure the state well

Nivetha Krishnan
Bits and Pieces

--

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 →

https://cdn-images-1.medium.com/max/800/1*ctBUj-lpq4PZpMcEF-qB7w.gif

Learn More

--

--