Principles Of Structuring State
Principles Of Structuring State
There are a few principles that can guide you to make better choices:
Group related state
If some two state variables always change together, it might be a good idea to unify them into a single state variable. Then you won’t forget to always keep them in sync.
// Instead of doing this
const [x, setX] = useState(0);
const [y, setY] = useState(0);
// This would be better
const [position, setPosition] = useState({ x: 0, y: 0 });
Avoid contradictions in state
When the state is structured in a way that several pieces of state may contradict and “disagree” with each other, you leave room for mistakes. Try to avoid this.
e.g. In below example, leaves the door open for “impossible” states. For example, if you forget to call setIsSent
and setIsSending
together after sendMessage
, you may end up in a situation where both isSending
and isSent
are true
at the same time.
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);
}
It is better to replace them with one status state variable that may take one of three valid states: 'typing'
(initial), 'sending'
, and 'sent'
,
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";
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.
In the below e.g. you can see fullName
is redundant.
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);
}
You can always calculate fullName
from firstName
and lastName
during render, so remove it from state.
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);
}
Avoid duplication in state
When the same data is duplicated between multiple state variables, or within nested objects, it is difficult to keep them in sync. Reduce duplication when you can.
In the below example, items
present in initialItems
is copied. Also one of the item that is selected is also copied as selectedItem
.
const initialItems = [
{ title: "pretzels", id: 0 },
{ title: "crispy seaweed", id: 1 },
{ title: "granola bar", id: 2 },
];
const [items, setItems] = useState(initialItems);
const [selectedItem, setSelectedItem] = useState(items[0]);
If somewhere in the code, if the value of title
in items
is changed. Then you have to have to update the value in selectedItem
as well, because of the duplication of the state.
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. This would be the better approach.
const initialItems = [
{ title: "pretzels", id: 0 },
{ title: "crispy seaweed", id: 1 },
{ title: "granola bar", id: 2 },
];
const [items, setItems] = useState(initialItems);
const [selectedId, setSelectedId] = useState(0);
const selectedItem = items.find((item) => item.id === selectedId);
Avoid deeply nested state
Deeply hierarchical state is not very convenient to update. When possible, prefer to structure state in a flat way.
Checkout the example here