Let’s get back to our profile screen and try to introduce the concept of state. The main purpose of this screen is to collect the user’s information. Because we want this data to be as accurate as possible, we have to make sure that what the user enters is as accurate and complete as possible — the age shouldn’t be a negative number, the name cannot be empty, and the height has to be in a reasonable range. This means that we have to put some validation in place and display proper error messages if it fails.

Once a user submits valid data, we have to store it in our database (or upload it to our server—or, often, both). This is usually a slow operation, so we have to disable the submit button and show a spinner until the process is complete.

All of the data and rules needed to present the UI at a specific point in time, all the different error messages, flags that indicate if we should show a spinner or disable a button, are considered its state, and they—not the UI—truly describe how our app behaves given a certain set of inputs.

When it comes to managing this state, we have a set of many best practices that we rely on. Here are two important ones:

Model state explicitly as a hierarchy of classes. Model state transition explicitly with reducers.

The main driver behind these principles is very simple: to understand what a component is doing, you have to follow the code, mentally mapping this component’s interactions with all of the other components.

For a complex system, keeping track of all of this in your head is, to say the least, hard to do. The naïve approach of mixing state management with the user interface code starts simple and seems convenient, but very quickly becomes unmanageable as the complexity of the application itself grows: just as you need state management to become easier so that you can focus on new features, it turns into a mess of buggy code that gets duplicated over and over and becomes impossible to maintain.

To avoid missing something critical, modern applications keep state and UI code separate, so that all the code that describes what data they need and how it changes their behavior is explicitly stored in a single, convenient location.

As you can imagine, you will have to write a bit more code this way; this makes this technique a little counterintuitive, which is probably one of the reasons why so many developers approach it with suspicion. However, it will help you to simplify your reasoning and make the system more understandable — which brings us to our first state management best practice.

Principle 1: Model State Explicitly as a Hierarchy of Classes

Let’s dig deeper into our profile feature. The requirements are simple: the user should be able to add or update their profile information. The default screen would look like this:

The data that we want to collect is gender, name, age, height, unit, and bio. We also have two business (validation) rules. First, everything except bio is required, and second, the user must be at least 18 years old.