Photo by Ferenc Almasi on Unsplash
Tips To Choose a STATE STRUCTURE Without Breaking A Sweat in React
Principles for structuring state
The shape of data and how many state variables you want to use when creating a component that holds state will be decisions you must make. Although you can write correct programs even with a suboptimal state structure, you can use the following principles to make better decisions:
Group Related State
You might sometimes be unsure between using a single or multiple state variables.
Do you need to do this?
const [x, setX] = useState(0); const [y, setY] = useState(0);
Is this better?
const [position, setPosition] = useState({ x: 0, y: 0 });
Both approaches are technically possible. It might make sense to merge two state variables into one if they change together all the time. Staying in sync will be easier then.
Objects cannot be updated with only one field without also updating the rest of the fields. For example, in the above example, you cannot do
setPosition([ x: 100 ])
since it would not have a y property! The alternative to setting x alone would be to do eithersetPosition([...position, x:100])
, or split them into two state variables andsetX(100)
.Avoid contradictions in state
Here is a hotel feedback form with
isSending
andisSent
state variables:
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);
});
}
There are "impossible" states that can occur with this code, even though it works. setIsSent
and setIsSending
may be called at the same time if you forget to call them together. You will have a harder time understanding what happened if your component is complex.
There should never be a state where both isSending
and isSent
are true simultaneously, so it is better to replace them with one state variable that can take one of three possible values: "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);
});
}
const isSending = status === 'sending';
const isSent = status === 'sent';
But they’re not state variables, so you don’t need to worry about them getting out of sync with each other.
Avoid redundant state
It is not a smart idea to put information into a component's state if you can calculate it from its props or existing state variables during rendering. Consider this form, for instance. Does it have any redundant states?
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 calculate fullName
from firstName
and lastName
during render. So, remove fullName
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>
</>
);
}
Here, fullName
is not a state variable. Instead, it’s calculated during render:
const fullName = firstName + ' ' + lastName;
Improving Memory Usage
Ideally, you would also remove the deleted items from the “table” object to improve memory usage. This version does that. ,. It also uses Immer to make the update logic more concise. .
This is how you can do it: .
App.js
import { useImmer } from 'use-immer';
import { initialTravelPlan } from './places.js';
export default function TravelPlan() {
const [plan, updatePlan] = useImmer(initialTravelPlan);
function handleComplete(parentId, childId) {
updatePlan(draft => {
// Remove from the parent place's child IDs.
const parent = draft[parentId];
parent.childIds = parent.childIds
.filter(id => id !== childId);
// Forget this place and all its subtree.
deleteAllChildren(childId);
function deleteAllChildren(id) {
const place = draft[id];
place.childIds.forEach(deleteAllChildren);
delete draft[id];
}
});
}
const root = plan[0];
const planetIds = root.childIds;
return (
<>
<h2>Places to visit</h2>
<ol>
{planetIds.map(id => (
<PlaceTree
key={id}
id={id}
parentId={0}
placesById={plan}
onComplete={handleComplete}
/>
))}
</ol>
</>
);
}
function PlaceTree({ id, parentId, placesById, onComplete }) {
const place = placesById[id];
const childIds = place.childIds;
return (
<li>
{place.title}
<button onClick={() => {
onComplete(parentId, id);
}}>
Complete
</button>
{childIds.length > 0 &&
<ol>
{childIds.map(childId => (
<PlaceTree
key={childId}
id={childId}
parentId={id}
placesById={placesById}
onComplete={onComplete}
/>
))}
</ol>
}
</li>
);
}
places.js
export const initialTravelPlan = {
0: {
id: 0,
title: '(Root)',
childIds: [1, 43, 47],
},
1: {
id: 1,
title: 'Earth',
childIds: [2, 10, 19, 27, 35]
},
2: {
id: 2,
title: 'Africa',
childIds: [3, 4, 5, 6 , 7, 8, 9]
},
3: {
id: 3,
title: 'Botswana',
childIds: []
},
4: {
id: 4,
title: 'Egypt',
childIds: []
},
5: {
id: 5,
title: 'Kenya',
childIds: []
},
6: {
id: 6,
title: 'Madagascar',
childIds: []
},
7: {
id: 7,
title: 'Morocco',
childIds: []
},
8: {
id: 8,
title: 'Nigeria',
childIds: []
},
9: {
id: 9,
title: 'South Africa',
childIds: []
},
10: {
id: 10,
title: 'Americas',
childIds: [11, 12, 13, 14, 15, 16, 17, 18],
},
11: {
id: 11,
title: 'Argentina',
childIds: []
},
12: {
id: 12,
title: 'Brazil',
childIds: []
},
13: {
id: 13,
title: 'Barbados',
childIds: []
},
14: {
id: 14,
title: 'Canada',
childIds: []
},
15: {
id: 15,
title: 'Jamaica',
childIds: []
},
16: {
id: 16,
title: 'Mexico',
childIds: []
},
17: {
id: 17,
title: 'Trinidad and Tobago',
childIds: []
},
18: {
id: 18,
title: 'Venezuela',
childIds: []
},
19: {
id: 19,
title: 'Asia',
childIds: [20, 21, 22, 23, 24, 25, 26],
},
20: {
id: 20,
title: 'China',
childIds: []
},
21: {
id: 21,
title: 'Hong Kong',
childIds: []
},
22: {
id: 22,
title: 'India',
childIds: []
},
23: {
id: 23,
title: 'Singapore',
childIds: []
},
24: {
id: 24,
title: 'South Korea',
childIds: []
},
25: {
id: 25,
title: 'Thailand',
childIds: []
},
26: {
id: 26,
title: 'Vietnam',
childIds: []
},
27: {
id: 27,
title: 'Europe',
childIds: [28, 29, 30, 31, 32, 33, 34],
},
28: {
id: 28,
title: 'Croatia',
childIds: []
},
29: {
id: 29,
title: 'France',
childIds: []
},
30: {
id: 30,
title: 'Germany',
childIds: []
},
31: {
id: 31,
title: 'Italy',
childIds: []
},
32: {
id: 32,
title: 'Portugal',
childIds: []
},
33: {
id: 33,
title: 'Spain',
childIds: []
},
34: {
id: 34,
title: 'Turkey',
childIds: []
},
35: {
id: 35,
title: 'Oceania',
childIds: [36, 37, 38, 39, 40, 41,, 42],
},
36: {
id: 36,
title: 'Australia',
childIds: []
},
37: {
id: 37,
title: 'Bora Bora (French Polynesia)',
childIds: []
},
38: {
id: 38,
title: 'Easter Island (Chile)',
childIds: []
},
39: {
id: 39,
title: 'Fiji',
childIds: []
},
40: {
id: 40,
title: 'Hawaii (the USA)',
childIds: []
},
41: {
id: 41,
title: 'New Zealand',
childIds: []
},
42: {
id: 42,
title: 'Vanuatu',
childIds: []
},
43: {
id: 43,
title: 'Moon',
childIds: [44, 45, 46]
},
44: {
id: 44,
title: 'Rheita',
childIds: []
},
45: {
id: 45,
title: 'Piccolomini',
childIds: []
},
46: {
id: 46,
title: 'Tycho',
childIds: []
},
47: {
id: 47,
title: 'Mars',
childIds: [48, 49]
},
48: {
id: 48,
title: 'Corn Town',
childIds: []
},
49: {
id: 49,
title: 'Green Hill',
childIds: []
}
};
Conclusion
- If two state variables always update together, consider merging them into one.
- Choose your state variables carefully to avoid creating “impossible” states.
- Structure your state in a way that reduces the chances that you’ll make a mistake updating it.
- Avoid redundant and duplicate state so that you don’t need to keep it in sync.
- Don’t put props into state unless you specifically want to prevent updates.
- For UI patterns like selection, keep ID or index in state instead of the object itself.
- If updating deeply nested state is complicated, try flattening it.