21 Why They Say Redux Can Replace Singleton State Management

21 Why They Say Redux Can Replace Singleton State Management #

Hello, I am Ishikawa.

Time flies. Today, we are going to start the third module of this series called “The Art of JavaScript.” In the previous modules, we have learned about functional and object-oriented programming paradigms, as well as data structures and algorithms in JavaScript. Starting from this lesson, we will delve into the design patterns used in JavaScript and explore how they can help improve productivity and optimize production relationships, while also incorporating some third-party libraries.

Typically, when discussing design patterns, we start with the creational patterns. Among the creational patterns, the singleton pattern is probably the most well-known, and it is often used for state management in many programming languages. In this lesson, we will examine the advantages and disadvantages of this pattern and how Redux, a library in JavaScript, addresses some of the challenges faced by singleton pattern in state management. In addition to the singleton pattern, I will also discuss several other creational patterns. Let’s start with the singleton pattern itself.

Singleton Pattern #

Before ES6, the Singleton pattern in JavaScript was often used for encapsulation and namespace, rather than state management. However, after ES6, JavaScript has introduced many ways to prevent global pollution. For example, the new let and const keywords keep variables declared with them within the block scope, reducing the use of global variables. Additionally, the new modular system in JavaScript makes it easier to create globally accessible values without polluting the global scope, as it exports values from modules and imports them into other files. Therefore, for developers, the remaining use case of the Singleton pattern is state management.

The Singleton pattern is primarily used to share a global instance within an application. This means that when creating two objects using the same class, the second object should be the same as the first one. In JavaScript, creating an object using object literals can be seen as implementing a Singleton. When you create a new object, it is already a Singleton object. Why is this the case? Because in JavaScript, objects are never equal unless they are the same object. Therefore, even if we create two objects with exactly the same properties, they will not be equal:

var obj1 = {
    myprop: 'my value'
};
var obj2 = {
    myprop: 'my value'
};
obj1 === obj2; // false

By the following example, we can see a simple implementation of a Singleton:

image

First, we create a global counter and a counter object that contains increment and decrement methods. To prevent this counter from being modified, we can use the freeze method to freeze the object. Finally, we export the counter component. If different modules import and call the same counter component for increment and decrement operations, they will get the same counter-based results.

var count = 0;

var counter = {
  increment() {
    return ++count;
  },
  decrement() {
    return --count;
  }
};

Object.freeze(counter);
export { counter };

State Management in Singleton Pattern #

When discussing functional programming patterns, we mentioned that one important concept is to consider side effects. Singleton pattern is often considered an anti-pattern because it is based on the consideration of side effects.

We have previously mentioned that object-oriented and functional design patterns are not mutually exclusive concepts, but rather complementary. A common use case of singleton is to have some kind of global state throughout the entire application. This can lead to conflicts when multiple parts of the codebase that depend on the same mutable object modify it. Since some parts of the codebase usually modify the values in the global state, while others use the data, the order of execution is also a consideration. We do not want to accidentally consume the data before it arrives, leading to an exception. In relatively complex programs, as the number of components and their interdependencies grow, it becomes difficult to manage global state between different data flows.

State Management in Functional Programming #

So how do we deal with the above-mentioned problems? In React, we often use state management tools like Redux or React Context to manage global state instead of using singleton objects. These tools employ many functional programming concepts to replace the singleton pattern and address these side effects. Let’s take a look at how this solution is implemented.

Although Redux or React Context also have global state and the behavior may appear similar to singleton, these tools provide read-only state instead of a singleton mutable state. For example, when using Redux, only pure function reducers can update the state after an action is dispatched in a component. Although using these tools cannot completely eliminate the drawbacks of global state, at least it guarantees that the global state changes as expected because components cannot directly update the state.

The intent of Redux can be summarized by three principles: first, all global states are stored in a single store; second, the state in this store is read-only for the entire application; third, if the state needs to be updated, it must be done through reducers.

Let’s take an example. In the UI interface shown below, there is a display feature for deposits, showing the current deposit as 0. There are also two buttons on the interface, one for depositing and one for withdrawing. At the beginning, when someone clicks the action of depositing 10 yuan, a storage event is sent to the event handler.

image

At this point, the event handler will package the data related to the behavior into an action object, which contains a field for the action type. You can think of the action object as an object that describes the events that occur in the application. In this example, the type is “deposit” and the corresponding payload’s record is “10”. After the action object is packaged, it is dispatched to the store.

image

At this point, the store will call the existing state, which means the current deposit amount is 0. Adding 10 yuan to it, the sum is 10 yuan. The reducer is a function that receives the current state and action object. If necessary, it decides how to update the state and returns the new state (state, action) => newState. You can think of the reducer as an event listener that processes events based on the received action type.

The reducer follows some specific rules that are worth noting: first, the reducer calculates the new state value based only on the state and action parameters; second, it must adhere to the principle of immutability and cannot modify the existing state, but only copy the state and make modifications on the copied version; third, the reducer avoids any side effects, such as asynchronous operations.

image

In addition, Redux has a method called dispatch. The only way to update the state is to call store.dispatch() and pass in an action object. The store will run the reducer function and save the new state value inside it. We can call getState() to retrieve the updated state.

Redux also has a Selector, which is a function that knows how to extract specific information from the stored state values. As the application grows larger, this helps avoid duplicate logic because different parts of the application need to read the same data. Finally, when the work in the store is completed, the UI obtains the updated deposit value of 10 yuan.

image

Redux follows a “one-way data flow” application structure. That is, first, the state describes the state of the application at a certain point in time, and the UI renders based on this state. Second, when certain events occur in the application, such as a scheduling action in the UI, the store runs the reducer, the state updates based on the events that occur, and the store notifies the UI that the state has changed, the UI re-renders based on the new state. Here we can see that although the design philosophy of Redux cannot completely eliminate side effects in state management, it is much more effective than the singleton pattern in many respects.

Factory Pattern #

In addition to singleton, today I will introduce two other common creational patterns: the factory pattern and the prototype pattern. Let’s first take a look at the factory pattern. The factory pattern uses a factory function to create objects. It allows us to call a factory instead of directly using the new operator or Object.create() to create new objects from a class. In JavaScript, the factory pattern is simply a function that returns an object without using the new keyword.

The factory pattern allows us to separate the creation of objects from their implementation. Essentially, the factory wraps the creation of a new instance, providing us with more flexibility and control. Inside the factory, we can choose to create a new instance of a class using the new operator, or dynamically build a stateful object literal using closures, or even return different object types based on specific conditions. The consumer of the factory has no knowledge of how the instance is created. In fact, by using new, we bind the code to a specific way of creating objects, whereas with a factory, we have more flexibility.

Its advantage is that the factory pattern is useful if we need to create relatively complex and configurable objects. The key-value pairs in the object depend on the specific environment or configuration. With the factory pattern, we can easily create new objects with custom keys and values. When we have to create multiple smaller objects that share the same properties, the factory function can easily return custom objects based on the current environment or user-specific configuration. On the other hand, its disadvantage is that it may consume more memory. In relatively less complex situations, creating new instances instead of new objects each time may save space.

The factory pattern is widely used in JavaScript. For example, Object() itself is like a factory because it creates different objects based on the input. If you pass a number to it, it can create an object using the Number() constructor in the background. The same goes for strings and boolean values. Any other value, including null, will create a plain object. Here is an example. The fact that Object() is also a factory has no practical use, but through this example, you can see that the factory pattern is ubiquitous in JavaScript.

var o = new Object(),
    n = new Object(1),
    s = Object('1'),
    b = Object(true);
// test
o.constructor === Object;  // true
n.constructor === Number;  // true
s.constructor === String;  // true
b.constructor === Boolean; // true

Another very common example is arrow functions being a factory pattern. This is because if the body of the arrow function consists of a single expression, it indirectly returns an object when the function is created, making it a small factory function.

var createUser = (userName) => ({ userName: userName });
createUser("bar"); // {userName: 'bar'}
createUser("foo"); // {userName: 'foo'}

Prototype Pattern #

The prototype pattern is not unfamiliar to JavaScript. The prototype is used to share properties among many objects of the same type.

If we look at it from a non-JavaScript perspective, many other languages introduce classes in their explanation of the prototype pattern. However, in the case of JavaScript, prototype inheritance can avoid the use of classes. This is done by leveraging its own advantages instead of trying to mimic the features of other languages. The prototype pattern not only simplifies the implementation of inheritance, but also improves performance. When functions are defined in objects, they are created by reference (so all child objects point to the same function) instead of creating individual copies. We have already discussed the prototype pattern in detail in the previous section on object-oriented programming, so we won’t go into detail here.

Summary #

In conclusion, let’s do a brief summary. Today I have introduced you to several different creational design patterns. I hope you have gained a different perspective on these patterns, such as how we can address related state management issues in frontend applications when dealing with singleton problems. JavaScript doesn’t just rely on traditional singletons and related solutions for state management. It takes a different approach by leveraging the ideas of functional programming, cleverly utilizing pure functions, immutability, and other characteristics to solve problems more effectively. Therefore, we can say that no design pattern is set in stone, but rather can be flexibly applied in specific situations.

Thought-provoking Question #

Here’s a thought-provoking question for you. When it comes to singleton pattern, we have seen how Redux solves the problem of state management. However, JavaScript is not only used on the frontend, but also on the backend, for example in Node, where there are many use cases and problems related to these creational patterns. Can you share your application scenarios, the related problems you have encountered, and some solutions?

Feel free to share your answers, exchange learning experiences, or ask questions in the comment section. If you find it helpful, you are also encouraged to share today’s content with more friends. See you in the next issue!