📜 ⬆️ ⬇️

Can I use Redux on the server?

Redux is an excellent tool for managing the state of complex front-end applications. The author of the material, the translation of which we are publishing today, is going to find an answer to the question of whether it is possible to use the Redux capabilities in a server environment.
image

Why do you need Redux library?


The home page of the Redux library says that this is a “predictable state container for JavaScript applications.” Redux is usually referred to as an application state management tool, and, although this library is mainly used with React, it can be used in any JavaScript-based projects.

We already mentioned that Redux is used to manage the state of an application. Now let's talk about what “state” is. This concept is quite difficult to define, but we still try to describe it.

Considering the “state”, if we are talking about people or objects of the material world, we strive to describe, in fact, their state at the time in which we are talking about them, perhaps considering one or several parameters. For example, we can say about the lake: “The water is very hot”, or: “The water is frozen”. In these statements we describe the state of the lake in terms of its water temperature.

When someone says about himself: “I am broke,” he considers the amount of money he has. It is clear that in each of these examples we are talking only about one aspect of the state of objects. But, in the example about money, the statement may be this, describing several parameters: "I broke, I have not eaten for a long time, but I am happy!". It is very important to note that the state is something non-permanent. This means that it can change. Therefore, when we learn about the current state of an object, we understand that its real state may change in a few seconds or minutes after we have learned about it.

When we deal with programs, the concept of "state" is associated with some features. First, the state of the application is represented by data that is stored somewhere. For example, this data can be stored in memory (say, as a JavaScript object), but it can be stored both in a file and in a database, and using the tools of some kind of caching mechanism like Redis. Secondly, the state of the application is usually tied to its specific instance. Therefore, when we talk about the state of an application, we mean a specific instance of this application, a process, a working environment, organized in an application for a specific user. Application state may include, for example, the following information:


If we talk about the state of the application at a lower level, it may include, for example, the following information:


Looking at the "snapshot" (they are often called "snapshots" - from snapshot) of the application state at any time, we can find out in what conditions the application worked at that moment, and, possibly, if necessary, recreate these conditions by giving application to the state in which it was at the time of snapshot.

The state can be modified during the execution of the user certain actions. For example, if a user correctly moves a game character in a simple game, this may increase the number of points. In fairly complex applications, the approach to state modification may become more complicated, and state changes may come from various sources.

For example, in a multiplayer game, how many points the user scores depends not only on his actions, but also on the actions of those who play with him on the same team. And if a computer-controlled character successfully attacks a game character that the user controls, the user may lose some points.

Imagine that we are developing a frontend application like Twitter PWA . This is a one-page application in which there are several tabs, say - Home Page, Search (Search), Notifications (Notifications) and Messages (Messages). Each such tab has its own working area, which is intended both for displaying certain information and for its modification. All this data forms the state of the application. So, new tweets, notifications and messages come into the app every few seconds. The user can work with the program and with this data. For example, he can create a tweet or delete it, he can retweet a certain tweet, he can read notifications, send messages to someone, and so on. Everything that was just discussed modifies the state of the application.


All of these tabs have their own set of user interface components used to display and modify data. The state of the application can be affected by data entering the application from the outside, and user actions

It is clear that in such an application, different entities can be sources of state changes, and changes initiated by different sources can occur almost simultaneously. If we manage the state manually, it may turn out that it will be difficult for us to keep track of what is happening. These difficulties lead to contradictions. For example, a tweet may be deleted, but it will still be displayed in the tweet feed. Or, say, a user may read a notification or message, but it will still be displayed in the program as unvisited.

The user can like a tweet, a heart appears in the program's interface, but a network request that sends information about the like to the server will not work. As a result, what the user sees will be different from what is stored on the server. It is in order to prevent such situations, and you may need Redux.

How does redux work?


There are three main concepts in the Redux library that are designed to make managing the state of applications simple and straightforward:

  1. Storage (store). Redux storage is a JavaScript object that represents the state of the application. It plays the role of "the only source of reliable data." This means that the entire application must rely on storage as the only entity responsible for the state representation.
  2. Actions The state store is read only. This means that it cannot be modified by referring to it directly. The only way to change the contents of the repository is to use actions. Any component that wants to change a state should use the appropriate action.
  3. Reducers (reducers), which are also called "converters". A reducer is a pure function that describes how a state is modified by actions. The reducer accepts the current state and the action, the execution of which has been requested by a certain component of the application, and then returns the transformed state.

Using these three concepts means that the application should no longer directly monitor events that are sources of state changes (user actions, API responses, the occurrence of events related to receiving some data via the WebSocket protocol, and so on) and make decisions about how these events will affect the state.

Through the use of the Redux model, these events can trigger appropriate actions that will change state. Components that need to use data stored in the application state can simply subscribe to state changes and receive information of interest. Using all of these mechanisms, Redux aims to make changes in the state of an application predictable.

Here is a schematic example that demonstrates how you can organize a simple state management system using Redux in our fictional application:

import { createStore } from 'redux'; // редьюсер const tweets = (state = {tweets: []}, action) => {  switch (action.type) {    // Мы обрабатываем лишь одно действие, выполняемое при поступлении нового твита.    case 'SHOW_NEW_TWEETS':      state.numberOfNewTweets = action.count;      return state.tweets.concat([action.tweets]);    default:      return state;  } }; // Вспомогательная функция, с помощью которой создаётся действие. SHOW_NEW_TWEETS const newTweetsAction = (tweets) => {  return {      type: 'SHOW_NEW_TWEETS',      tweets: tweets,      count: tweets.length  }; }; const store = createStore(tweets); twitterApi.fetchTweets()  .then(response => {    // Вместо того, чтобы вручную добавлять твит в соответствующее место программы,    // мы отправляем действие Redux.    store.dispatch(newTweetsAction(response.data));  }); // Кроме того, мы используем действие SHOW_NEW_TWEETS когда пользователь создаёт твит // в результате твит пользователя тоже добавляется в состояние приложения. const postTweet = (text) => {  twitterApi.postTweet(text)  .then(response => {    store.dispatch(newTweetsAction([response.data]));  }); }; // Предположим, в приложение, по протоколу WebSocket, поступили новые твиты. // При возникновении этого события мы тоже можем отправить действие. SHOW_NEW_TWEETS socket.on('newTweets', (tweets) => { store.dispatch(newTweetsAction(tweets)); }; // Если мы используем некий фреймворк, вроде React, то наши компоненты нужно подключить к хранилищу, // нужно, чтобы они автоматически обновлялись бы для вывода новых твитов. // В противном случае нам нужно самостоятельно настроить прослушивание событий, // возникающих при изменении состояния. store.subscribe(() => {  const { tweets } = store.getSTate();  render(tweets); }); 

Taking this code as a basis, we can equip our state management system of the application with additional actions and send them from different places of the application without risking hopelessly confused.

Here is the material from which you can learn more about the three fundamental principles of Redux.

Now let's talk about using Redux in a server environment.

Transferring Redux principles to the server environment


We explored the Redux capabilities used in developing client applications. But, since Redux is a JavaScript library, it can theoretically be used in a server environment. Let us consider how the above principles can be applied on the server.

Remember how we talked about what the state of the client application looks like? It should be noted that there are some conceptual differences between client and server applications. For example, client applications tend to maintain state between various events, say, between the execution of requests to the server. Such applications are called stateful applications.

If they did not strive for state storage, for example, when working with a certain web service that requires entering a login and password, the user would have to perform this procedure every time he goes to a new page of the corresponding web interface.

Backend applications, on the other hand, tend to ensure that the state is not stored (they are also called stateless applications). Here, speaking of "backend applications", we mainly mean projects based on certain APIs that are separate from front-end applications. This means that information on the state of the system should be provided to similar applications each time they are accessed. For example, the API does not monitor whether a user is logged in or not. It determines its status by analyzing the authentication token in its requests to this API.

This leads us to the important reason that Redux would hardly be used on servers in the form in which we described its capabilities above.

The fact is that Redux was designed to store the temporary state of the application. But the state of the application stored on the server should usually be long enough. If you would use the Redux repository in your server-side Node.js application, then the state of this application would be cleared every time the node process stops. And if we are talking about a PHP server that implements a similar state management scheme, the state would be cleared when each new request arrives at the server.

The situation becomes even more complicated if we consider server applications from the point of view of their scalability. If you had to scale the application horizontally, increasing the number of servers, then you would have a lot of Node.js processes running simultaneously, and each of them would have its own version of the state. This means that if two identical requests to the backend were received simultaneously, different answers could be given to them.

How to apply the principles of state management discussed on the server? Take another look at the Redux concepts and see how they are commonly used in a server environment:

  1. Storage. On the back end, “the only source of reliable data” is usually a kind of database. Sometimes, in order to facilitate access to data that is often required, or for some other reason, a copy of some part of this database can be made — as a cache or as a file. Usually such copies are read only. The mechanisms that control them are subscribed to changes in the main repository, and, if such changes occur, update the contents of the copies.
  2. Actions and reduers. They are the only mechanisms used to change a state. In most backend applications, the code is written in an imperative style, which is not particularly conducive to the use of concepts of actions and reducers.

Consider two design patterns that, by their nature, are similar to what the Redux library is aimed at. This is CQRS and Event Sourcing. They, in fact, appeared before Redux, their implementation can be extremely difficult, so we will talk about them very briefly.

CQRS and Event Sourcing


CQRS (Command Query Responsibility Segregation, division of command and query responsibility) is a design pattern that, when implemented, an application reads data from the repository only with queries, and writes only with commands.

When using CQRS, the only way to change the state of an application is to send a command. Commands are similar to Redux actions. For example, in Redux, you can write code that corresponds to this scheme:

 const action = { type: 'CREATE_NEW_USER', payload: ... }; store.dispatch(action); // реализовать редьюсер для выполнения действия const createUser = (state = {}, action) => { // }; 

When using CQRS, something like this will look like this:

 // базовый класс или интерфейс команды class Command { handle() { } } class CreateUserCommand extends Command { constructor(user) {   super();   this.user = user; } handle() {   // создать запись о пользователе в базе данных } } const createUser = new CreateUserCommand(user); // отправить команду (это вызовет метод handle()) dispatch(createUser); // или здесь можно воспользоваться классом CommandHandler commandHandler.handle(createUser); 

Queries are the mechanisms for reading data in the CQRS template. They are equivalent to store.getState() . In a simple implementation of CQRS, queries will interact directly with the database, getting records from it.

The Event Sourcing template (event registration) is designed with a view to registering all changes in the application state as a sequence of events. This template is best suited for applications that need to know not only about their current state, but also about its history of changes, about how the application has reached its current state. As examples here you can cite the history of operations with bank accounts, tracking parcels, work with orders in online stores, organization of transportation, logistics.

Here is an example of implementing the Event Sourcing pattern:

 // без использования шаблона Event Sourcing function transferMoneyBetweenAccounts(amount, fromAccount, toAccount) {   BankAccount.where({ id: fromAccount.id })     .decrement({ amount });   BankAccount.where({ id: toAccount.id })     .increment({ amount }); } function makeOnlinePayment(account, amount) {   BankAccount.where({ id: account.id })     .decrement({ amount }); } // с использованием шаблона Event Sourcing function transferMoneyBetweenAccounts(amount, fromAccount, toAccount) {   dispatchEvent(new TransferFrom(fromAccount, amount, toAccount));   dispatchEvent(new TransferTo(toAccount, amount, fromAccount)); } function makeOnlinePayment(account, amount) {   dispatchEvent(new OnlinePaymentFrom(account, amount)); } class TransferFrom extends Event {   constructor(account, amount, toAccount) {     this.account = account;     this.amount = amount;     this.toAccount = toAccount;   }     handle() {     // сохранить новое событие OutwardTransfer в базе данных     OutwardTransfer.create({ from: this.account, to: this.toAccount, amount: this.amount, date: Date.now() });         // и обновить текущее состояние счёта     BankAccount.where({ id: this.account.id })       .decrement({ amount: this.amount });   } } class TransferTo extends Event {   constructor(account, amount, fromAccount) {     this.account = account;     this.amount = amount;     this.fromAccount = fromAccount;   }     handle() {     // сохранить новое событие InwardTransfer в базе данных     InwardTransfer.create({ from: this.fromAccount, to: this.account, amount: this.amount, date: Date.now() });         // и обновить текущее состояние счёта     BankAccount.where({ id: this.account.id })       .increment({ amount: this.amount });   } } class OnlinePaymentFrom extends Event {   constructor(account, amount) {     this.account = account;     this.amount = amount;   }     handle() {     // сохранить новое событие OnlinePayment в базе данных     OnlinePayment.create({ from: this.account, amount: this.amount, date: Date.now() });         // и обновить текущее состояние счёта     BankAccount.where({ id: this.account.id })       .decrement({ amount: this.amount });   } } 

What is happening here also reminds us of working with Redux actions.

However, the event registration mechanism also organizes the long-term storage of information about each state change, and not just the storage of the state itself. This allows us to reproduce these changes to the point in time we need, thus restoring the contents of the state of the application to this point in time. For example, if we need to understand how much money was in a bank account on a certain date, we only need to reproduce the events that occurred with the bank account, until we get to the desired date. Events in this case are represented by receipts of funds to the account and debiting them from it, writing off a bank commission and other similar operations. In the event of errors (that is, in the event of events containing incorrect data), we can invalidate the current state of the application, correct the corresponding data and return to the current state of the application, which has already been generated without errors.

CQRS and Event Sourcing templates are often used together. And, interestingly, Redux is, in fact, partly based on these templates. Commands can be written so that when they are invoked, they send events. The events then interact with the repository (database) and update the state. In real-time applications, query objects can also listen for events and receive updated status information from the repository.

Using any of these templates in a simple application may unnecessarily complicate it. However, in the case of applications built to solve complex business problems, CQRS and Event Sourcing are powerful abstractions that help better model the subject area of ​​such applications and improve their state management.

Note that the CQRS and Event Sourcing templates can be implemented differently, with some of their implementations being more complex than others. We have considered only very simple examples of their implementation. If you are writing server applications in Node.js, take a look at wolkenkit . This framework, among those that have been discovered in this area, provides the developer with one of the simplest interfaces for implementing CQRS and Event Sourcing templates.

Results


Redux is a great tool for managing the state of an application, in order to make state changes predictable. In this article we talked about the key concepts of this library and found that, although using Redux in a server environment is probably not the best idea, similar principles can be applied to the server using the CQRS and Event Sourcing templates.

Dear readers! How do you organize state management of client and server applications?

Source: https://habr.com/ru/post/437804/