The more complex the frontend application, the more different elements it contains. Each of these elements reacts to what's going on around them in one way or another: spinners spin, buttons are pressed, menus appear and disappear, and data is sent.
Ideally, any change in the interface is a consequence of a change to some data, i.e., a change to the relevant state in the application. Imagine a registration form where the submit button is blocked while a request is being made to the server (from UX perspective, this is a must for all forms). In this case, the state may take the following form:
const state = {
registrationProcess: {
valid: true,
submitDisabled: true,
isLoading: false,
}
};
In real-world applications, it's even more complicated. While sending data, not only will the send button be disabled, but also the input field. Moreover, sending data in one place can affect the rest of the blocks on the page; they may disappear, be blocked or change. Not to mention the fact that there may be several reasons for disabling the button. It may be disabled simply because incorrect data was entered into the form.
If you solve this problem head-on, you'll get a state with a lot of flags:
const state = {
registrationProcess: {
valid: true,
submitDisabled: true,
inputDisabled: true,
showSpinner: true,
blockAuthentication: true,
}
};
Each flag is responsible for a different element on the screen. As the number of flags grows, the logic for updating the state will become more complex (you need to coordinate them with each other and not forget to update them) as will the output logic (external outputs will start to have different dependencies on different flags).
The problem with this approach is that it doesn't rely on the causes of what's happening, but on their consequences. Changing whether buttons are active, locking elements, displaying spinners - all these are the consequence of various processes. The ability to isolate these processes and properly define them in the state is one of the cornerstones of good architecture.
In the example above, most of the flags are related to the processing of form data. Suppose that after sending the form, the data goes to the server, the server sends a response, and then the result is displayed to the user. The result may or may not be successful. We have to think through all the outcomes. The whole process can be usually divided into several intermediate states:
The set we've suggested here is not universal. Processes can be more complex, which means a different set of states will be required. And the names of the states are participles.
- filling – filling out a form. In this state, everything is active and available for editing.
- processing (or sending) – sending the form. This is the same state as when the user is waiting and the application tries to prevent unwanted actions, such as clicks or changes to form data.
- processed (or finished) – the state indicating that everything is finished. The form will no longer be displayed.
- failed – the state that says that there was an error. For example, if there was a network failure while downloading or the downloaded data was incorrect.
In terms of automata theory (and in this case, we're dealing with automata-based programming), such states are called control states. They define where we are now. Let's rewrite our state:
const state = {
registrationProcess: {
state: 'filling',
}
};
Even this seemingly small change dramatically simplifies the system. Now we don't have to keep track of every element involved. The main thing is that all possible states describe all possible behaviors. Then all the checks in the output will be reduced to a check of the general state:
// There can be as many of these “ifs” as you like,
// the main thing is that they are tied to the general state, and not not checks for specific flags
if (state.registrationProcess.state === 'processing') {
// Disabling buttons
// Activating spinners
}
if (state.registrationProcess.state === 'failed') {
// Output an error message
}
In addition to these states, there are various data that accompany our process. For example, processed may end with errors. In this case, you can enter an additional array (or object, depending on the structure) with the errors, which will be filled as they occur:
const state = {
registrationProcess: {
errors: ['Name not filled in', 'Address has the wrong format'],
state: 'processed',
}
};
And this error array is also convenient to use to validate the form before sending it to the server. That is, being in the filling state.
What if we want to disable the ability to submit a form until the frontend is validated? There are two approaches: either we verify that the errors is empty, or, even better, we introduce an explicit form validity state. And then our application's state becomes:
const state = {
registrationProcess: {
errors: ['Name not filled in', 'Address has an invalid format'],
state: 'processed',
validationState: 'invalid' // or valid
}
};
In some situations, it's possible to combine them when the validation process is connected with the registration process.Then instead of a separate validationState, there will be an additional invalid inside the state. It's not exactly correct from a modeling standpoint (because we really have two different processes), but this can sometimes help you simplify your code (as long as there aren't too many differences).
Globally, this approach in development is called programming with an explicit allocated state. It boils down to the fact that the application contains the basic processes that everything else depends on. These processes are then modeled using finite state machines (FSMs). It doesn't matter what tools are used for development: pure DOM, jQuery or any powerful modern framework. It's applicable everywhere and needed everywhere.
This is an incredibly powerful programming paradigm, which is described in the book "Automata-Based Programming" in our references.
Recommended materials
Are there any more questions? Ask them in the Discussion section.
The Hexlet support team or other students will answer you.
- Article “How to Learn and Cope with Negative Thoughts“
- Article “Learning Traps“
- Article “Complex and simple programming tasks“
For full access to the course you need a professional subscription.
A professional subscription will give you full access to all Hexlet courses, projects and lifetime access to the theory of lessons learned. You can cancel your subscription at any time.