Choose the State Structure
Effectively structuring state can be the difference between a component that is easy to modify and debug and one that is a persistent source of bugs. Here are some tips to consider when organizing state.
Principles for structuring state
When you write a component that holds state, you will make choices about how many state variables to use and what the shape of their data should be. While it is possible to write correct programs even with a suboptimal state structure, there are a few principles that can help you to make better choices:
- Group related state. If you always update two or more state variables at the same time, consider merging them into a single state variable.
- 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.
- 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.
- 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.
- Avoid deeply nested state. Deeply hierarchical state is not very convenient to update. When possible, prefer to structure state in a flat way.
These principles aim to simplify state updates and minimize errors. By eliminating redundant and duplicate data from the state, you can ensure consistency across all its pieces. This approach is akin to how a database engineer might “normalize” a database structure to minimize bugs.
Group related state
At times, you may be uncertain whether to use a single state variable or multiple state variables.
Should you use this?
Or should you use this?
You can use either approach, but if two state variables always change together, consider combining them into a single state variable.
Grouping data into an dictionary or list is useful when the number of state pieces is unknown. For instance, this approach is beneficial for forms where users can add custom fields.
When your state variable is an object, you must copy the other fields explicitly when updating a single field. For instance, using set_date_range({ "start": "2020-02-03" }) in the example above would omit the end field. To update only x, use set_date_range({ **date_range, "start": "2020-02-03" }) or separate them into two state variables and use set_start("2020-02-03").
Avoid contradictions in state
Here is a feedback form with is_sending and is_sent state variables:
Although this code functions, it allows for “impossible” states. For instance, if you forget to call set_is_sent and set_is_sending together, you might end up with both is_sending and is_sent being True simultaneously. The more complex your component becomes, the harder it is to trace what went wrong.
Since is_sending and is_sent should never be True at the same time, it is better to replace them with a single status state variable that can take one of three valid states: typing (initial), sending, and sent:
You can still declare some constants for readability:
However, they are not state variables, so you do not need to worry about them getting out of sync with each other.
Avoid redundant state
If you can derive some information from the component’s props or its existing state variables during rendering, you should avoid putting that information into the component’s state.
For instance, consider this form. It functions correctly, but there is state within it.
This form has three state variables: first_name, last_name, and full_name. However, full_name is redundant because it can be derived from first_name and last_name during render. Therefore, you should remove full_name from the state.
Here is how you can do it:
Here, full_name is not a state variable. Instead, it is calculated during render:
Therefore, the change handlers can update it without any special actions. Calling set_first_name or set_last_name triggers a re-render, and the full_name will be recalculated using the updated data.
Avoid duplication in state
This component lets you choose a single snack out of several:
The selected item is currently stored as a dictionary in the selected_item state variable. However, this approach is not ideal because the selected_item contains the same object as one of the items in the items list. This results in duplicated information about the item in two places.
Why is this an issue? Let’s make each item editable:
Notice how if you first click “Choose” on an item and then edit it, the input updates, but the label at the bottom does not reflect the changes. This happens because you have duplicated state and forgot to update selected_item.
While you could update selectedItem as well, a simpler solution is to eliminate the duplication. In this example, instead of maintaining a selected_item dictionary (which duplicates the objects inside items), you store the selected_id in the state and then retrieve the selected_item by searching the items list for an item with that ID:
Previously, the state was duplicated like this:
After the change, it looks like this:
The duplication is removed, keeping only the essential state.
Now, if you edit the selected item, the message below updates immediately. This happens because set_items triggers a re-render, and selected_item = next(item for item in items if item["id"] == selected_id) locates the item with the updated title. You don’t need to store the selected item in the state, as only the selected ID is essential. The rest can be computed during render.
Avoid deeply nested state
Consider a travel itinerary that includes planets, continents, and countries. You might think to organize its state with nested objects and arrays, as shown in this example:
If you want to add a button for deleting a place you’ve visited, you need to update the nested state by making copies of dictionaries from the changed part upwards. Deleting a deeply nested place requires copying its entire parent chain, which can be verbose.
If the state is too nested, consider flattening it. One way to restructure the data is to have each place hold an array of its child place IDs, and store a mapping from each place ID to the corresponding place.
This restructuring is similar to a database table:
With the state now “flat” (or “normalized”), updating nested items is simplified.
To remove a place, you only need to update two levels of state:
- Update the parent place to exclude the removed ID from its
child_idsarray. - Update the root “table” object to include the updated version of the parent place.
Here’s an example of how to do it:
Nesting state is flexible, but keeping it “flat” can resolve many issues. A flat state is easier to update and helps prevent duplication across different parts of a nested object.