If you want to read this article in Japanese here is the link: https://zenn.dev/takuyakikuchi/articles/225bc3d88d8538
TL;DR
5 principles
- Always combine states that change together into one
- Manage related states to avoid conflicts
- Do not assign values to states that can be calculated from existing states or props
- Avoid duplication
- Avoid deep nesting, make it flat
Introduction
Structuring state well can make a difference between a component that is pleasant to modify and debug, and one that is a constant source of bugs.
Choosing the State Structure
Recently, I had the opportunity to refactor a React application's duplication of states and the associated improper use of useEffect()
.
I thought that if we had been aware of the five principles described in the article Choosing the State Structure, we could have prevented some of these technical debts.
The purpose of this article is to disseminate those five principles.
It is based on the key points of the original article, but organized in the author's own way to make it more accessible. Therefore, please note that it contains expressions based on the author's interpretation.
Five principles
1. Always combine states that change together into one
From Group related state
As in the coordinate example below, values that always change together should be managed in a single state.
This way, you don't have to worry about synchronization of values.
// π `x` and `y` always change together, but are managed in separate states
- const [x, setX] = useState(0);
- const [y, setY] = useState(0);
// π combine into one state.
+ const [position, setPosition]=useState({ x: 0, y: 0 }
2. Manage related states to avoid conflicts
From Avoid contradictions in state
To avoid contradictions in state, consider what values to keep in what states.
Avoiding contradictions reduces the risk of bugs.
export default function FeedbackForm() {
// π `isSending` and `isSent` can never be `true` at the same time, but that improbable state is possible
- const [isSending, setIsSending] = useState(false);
- const [isSent, setIsSent] = useState(false);
// π replace with a single state with one of the valid values ('typing' (initial state), 'sending', or 'sent')
+ const [status, setStatus] = useState('typing');
async function handleSubmit(e) {
e.preventDefault();
- setIsSending(true);
+ setStatus('sending');
await sendMessage(text);
- setIsSending(false);
- setIsSent(true); + setStatus('sent'); await
+ setStatus('sent'); }
}
...
3. Do not assign values to states that can be calculated from existing states or props
Reducing unnecessary state management simplifies the code.
Computing computable values each time at render time eliminates the risk of out-of-sync, reducing complexity and the risk of bugs.
export default function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
// π `fullName` can be computed from `firstName` and `lastName`, but we use state
- const [fullName, setFullName] = useState('');
// π compute `fullName` at render time.
+ const fullName = firstName + ' ' + lastName;
function handleFirstNameChange(e) {
setFirstName(e.target.value);
- setFullName(e.target.value + ' ' + lastName); }
}
function handleLastNameChange(e) {
setLastName(e.target.value); }
- setFullName(firstName + ' ' + e.target.value); }
}
...
}
Such a verbose state is also an example of inducing misuse of useEffect() π£
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
...
// π Update the `fullName` state using `useEffect()`.
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
...
From You might not need an effect
Misuse of useEffect()
can slow down your application due to extra re-rendering, make them more complex than necessary, and create the potential for bugs due to synchronization errors.
4. Avoid redundancy
Reducing the number of duplicate states makes management easier.
Keep only the necessary information in state.
const initialItems = [
{ title: 'pretzels', id: 0 }, }
{ title: 'crispy seaweed', id: 1 }, }
{ title: 'granola bar', id: 2 }
];; }
export default function Menu() {
const [items, setItems] = useState(initialItems);
// π Duplication occurs because the same object as one of the values of the `items` state is held in the `selectedItem`.
// `items = [{ id: 0, title: 'pretzels'}, ...] `
// `selectedItem = { id: 0, title: 'pretzels'}`
- const [selectedItem, setSelectedItem] = useState(items[0]);.
// π By keeping the `selectedId` in state, we avoid duplicates and keep only the state we need.
// `items = [{ id: 0, title:'pretzels'}, ...] ;
// `selectedId = 0`
+ const [selectedId, setSelectedId] = useState(0);
// π get selectedItem by searching the items array for the item with that ID.
+ const selectedItem = items.find(item =>
+ item.id === selectedId
+ );
function handleItemChange(id, e) {
setItems(items.map(item => {
if (item.id === id) {
return {
... .item,.
title: e.target.value,.
};
} else {
return item; }
}
}));
// π update `selectedItem` or it will be a bug
- setSelectedItem(...) ;
}
return (
<>
<h2>What's your travel snack?</h2>
<ul>
{items.map((item, index) => (
<li key={item.id}>
<input
value={item.title}
onChange={e => {
handleItemChange(item.id, e)
}}
/>
{' '}
<button onClick={() => {
setSelectedItem(item);
}}>Choose</button>
</li>
))}
</ul>
<p>You picked {selectedItem.title}. </p>
</>
);
}
5. Avoid deep nesting, make it flat
From Avoid deeply nested state
Complex nesting tends to be difficult to manage. If possible, rethink your data structure and make it flat.
// π Deep nest
- export const initialTravelPlan = {
- id: 0,
- title: '(Root)',
- childPlaces: [{
- // ζζγ¬γγ«
- id: 1,
- title: 'Earth',
- childPlaces: [{
- // 倧ιΈγ¬γγ«
- id: 2,
- title: 'Africa',
- childPlaces: [{
- // ε½γ¬γγ«
- id: 3,
- title: 'Botswana',
- childPlaces: []
- }, {
- ...
- }],
- }, {
- // 倧ιΈγ¬γγ«
- ...
- }],
- }, {
- // ζζγ¬γγ«
- ...
- }],
- };
// π Make it flat
+ export const initialTravelPlan = {
+ 0: {
+ id: 0,
+ title: '(Root)',
+ childIds: [1, 42, 46],
+ },
+ 1: {
+ id: 1,
+ title: 'Earth',
+ childIds: [2, 10, 19, 26, 34]
+ },
+ 2: {
+ id: 2,
+ title: 'Africa',
+ childIds: [3, 4, 5, 6 , 7, 8, 9]
+ },
+ 3: {
+ id: 3,
+ title: 'Botswana',
+ childIds: []
+ },
+ ...
+ };
Summary
5 principles
- Always combine states that change together into one
- Manage related states to avoid conflicts
- Do not assign values to states that can be calculated from existing states or props
- Avoid duplication
- Avoid deep nesting, make it flat
The Challenges section at the end of the original article provides a systematic understanding of these principles, and I encourage you to take the challenge!
https://react.dev/learn/choosing-the-state-structure
Happy coding!
Top comments (0)