Skip to main content

Principles Of Structuring State

Principles Of Structuring State

There are a few principles that can guide you to make better choices:

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