Click on the above Programmer Growth North, pay attention to the public account

Reply 1, join the advanced node communication group

Translator: @PP Xu Translation: Author: @frontendmastery Original:

Understand the core issues that the state management library needs to address. And how the flood of modern libraries that have sprung up is solving these problems in new ways.

As React applications continue to grow in size and complexity, managing the global state that can be shared has become a challenge. The general recommendation is to introduce global state management scenarios only when you really need them.

This article discusses in detail the core issues that the global state management library needs to address.

Understanding these potential issues will help us evaluate the trade-offs that these “new wave” of state management are making. For other aspects, it is better to start local and expand only when needed.

React itself doesn’t provide any clear guidance on how to address global app state sharing. Therefore, over time, the React ecosystem has accumulated a lot of methods and libraries to solve this problem.

So this can be confusing when evaluating which library or pattern to use.

A common approach is to put it on the outer layer and use the most mainstream tools available today. That’s what we’re seeing, and that’s what was the case with widespread use of Redux in the early days, when many applications didn’t need it.

Understanding the problems with the use of the state management library can help us better understand why so many different libraries take different approaches.

Each library makes a number of different trade-offs in solving different problems, resulting in many differences in APIs, patterns, and the conceptual model of the state of thought.

We’ll take a look at the modern methods and patterns used in Recoil, Jotai, Zusand, Valtio, and other libraries like React tracked and React Query. See how they adapt to the environment.

Finally, when we need to choose a library that is really useful for our application, we should be more prepared to accurately evaluate the trade-offs in the implementation of this library.

1. “Ability to read storage status from anywhere in the component tree.” This is the most basic function of the state management library.

It allows developers to save state in memory and avoid the problem of passing a large number of properties. In the early days of the React ecosystem, we often used Redux inappropriately to address this pain point.

In fact, there are two main approaches when it comes to actually storing state.

The first is inside the React runtime. This typically refers to using APIs such as useState, useRef, or useReducer provided by React and passing shared values in conjunction with the React context. The biggest challenge here is how to properly optimize the duplicate rendering problem.

The second is a problem outside of the React knowledge body, called module state. Module state allows state to be stored as a singleton-like form. This makes it easier to optimize for duplicate rendering issues and only need to selectively handle subscriptions when the state changes. But because it is a single value in memory, different subtrees cannot have different states.

2. “Ability to write to the storage state.” A library should provide an intuitive API to read and write data from storage.

An intuitive API is typically an API that conforms to an existing mental model. So this can be a bit subjective depending on who the users of the library are.

Usually, conflicts in mental models can lead to resistance to use or increase learning costs. Common mental model conflicts in React are mutable and immutable.

The model in React that uses the UI as a state function applies to the concepts of reference equality and detecting when changes are made by immutable updates for proper rerendering. But Javascript itself is a mutable language.

When using React, we have to keep in mind things like equal references. This can be a source of confusion for Javascript developers who aren’t used to functional concepts and increase the cost of learning React.

Redux follows this model and requires that all status updates be completed in an immutable manner. Making such a choice requires a trade-off. A common drawback in this case is that those accustomed to updating in a variable way have to write a lot of boilerplate code to update.

That’s why libraries like Immer are so popular that they allow developers to write code in a mutable form (immutable even with the underlying update).

In the new wave of “post-redux” global state management scenarios, there are also libraries, such as Valtio, that allow developers to use mutated forms of APIs.

3. “Provide a mechanism to optimize rendering.” A model that uses the UI as a state function should be simple and efficient.

However, the process of coordination when the state changes on a large scale is extremely complex. This often leads to runtime performance issues for large apps.

With this model, the global state management library needs to detect when re-rendering occurs when the state is updated, and re-render only what is necessary.

Optimizing this process is one of the biggest challenges that the state management library needs to solve.

There are two main approaches that are usually taken. The first is to allow developers to manually optimize the process.

An example of manual optimization is to subscribe to a small block of stored state through a selector function. Components that read state through this selector are only re-rendered when a particular state is updated.

The second approach is to handle this problem automatically for developers so that you don’t have to think about manual optimization.

Valtio is a sample library that uses proxy in the background to automatically track state changes and automatically manage when components are re-rendered.

4. “Provide a mechanism to optimize memory usage.” For large front-end applications, a large amount of unreasonable memory management can cause problems.

Especially when users access these large applications with low-configuration devices.

Hanging to a state on the React lifecycle means that it is easier to take advantage of the automatic garbage collection mechanism when the component is unloaded.

For a library like Redux that advocates a single global state, you need to manage it yourself. Because it keeps references to the data, garbage collection is not automatic.

Similarly, using the state management library to store state outside of the React runtime means that it does not depend on any specific components and may need to be managed manually.

“More questions to solve:” In addition to the above basic problems, there are some common problems to consider when integrating with React:

1. “Compatibility with concurrent mode.” Concurrency mode allows React to “pause” and switch priorities during rendering. Previously, this process was fully synchronized.

Introducing concurrency everywhere usually leads to some edge scenes. For the state management library, if two components read a value from an external store and that value changes during rendering, the two components may read different values.

This is called “tearing.” This problem led the React team to develop a useSyncExternalStore for library creators to solve the problem.

2. “Data serialization.” It’s useful to have a fully serializable state so that you can save and restore app state from a store. Some libraries will handle this for you, while others may require some extra work from the consumer to use this capability.

3. “Loss of context.” This is a problem for applications that mix multiple react renders together. For example, you might have a colleague who uses react-dom and a library like react-three-fiber. React cannot reconcile two separate contexts.

4. “Expired attribute problem.” Hooks solve many problems with traditional class components. The trade-off is to accept the new set of problems posed by closures.

A common problem is that the data in the closure is no longer “fresh” in the current render cycle. This causes the data rendered onto the screen not to be up-to-date. Problems arise when a selector function that uses these properties to calculate state is encountered.

5. “Zombie subcomponent problem.” This is an old problem with Redux that can cause data inconsistencies if a child component first mounts and connects to storage before the parent component, while a state change occurs before the parent component is mounted.

As we can see, the global state management library requires a lot of issues and edge scenarios to consider.

To better understand the modern approach to React state management. We can recall history and see what pain points in the past shaped what we call “best practices” today.

Often, these best practices are discovered through trial and error through trial and error. And find that some solutions don’t work well in the end.

From the beginning, the original tagline of React’s initial release was to locate the “view” in the MVC model.

It doesn’t contain a point of view on how to structure or manage state. This means that developers are on their own when dealing with the most complex parts of a front-end application.

Inside Facebook, a pattern called “Flux” is used, which facilitates one-way data streams and predictable updates, which is consistent with React’s “always re-rendered” model.

This model fits very well with React’s mental model and became popular early in the React ecosystem.

Redux was one of the first implementations of the widely adopted Flux model.

It advocates the use of a single store, inspired in part by the Elm architecture rather than the multiple storage common in other Flux implementations.

When you start a new project, you are not fired for choosing Redux as the state management library. It also has cool presentation capabilities, such as the handy implementation of undo/redo features and time travel debugging capabilities.

The entire model is still simple and elegant. Especially compared to React’s previous generation of MVC-style frameworks such as Backbone.

While Redux is still a great state management library for specific scenarios. But over time, and as the community as a whole grew, Redux ran into some common problems that made it no longer popular:

1. Problems in small applications

For many of the early applications, it solved the first problem. Access storage state from anywhere in the tree, avoiding the pain of passing data and functions to update data to multiple levels.

This is often too heavy for simple apps that take a small amount of data and have little interaction.

2. Problems in large applications

Over time, many small applications have gradually become large applications. As we have found in practice, there are many different types of states in front-end applications. Each has its own set of questions.

For example, local UI state, remote server cache state, url state, global shared state, and many more different types of state.

For example, for local UI state, passing properties in data and methods of updating data can often quickly become an issue as the app evolves. To solve this problem, using a combination of component composition mode and state improvement can help you get through this period better.

There are some common problems with remote server cache state, such as request deduplication, retry, polling, handling mutations, and so on.

As applications evolve, Redux tends to absorb all state, regardless of type, because it advocates the use of a single store.

This results in everything being stored in a single oversized store. This tends to begins the second problem, runtime performance optimization.

Because Redux typically only handles global shared state, many of these sub-issues need to be dealt with repeatedly (or usually left unattended).

This results in a large, single store that manages everything between the UI and remote entity state in one place.

As the app grows, this of course becomes very difficult to manage. Especially in teams where front-end developers need to iterate quickly. Decoupling the handling of independent complex components becomes more necessary.

As we encountered more of these pain points, slowly, using Redux by default when launching new projects became unpopular.

In fact, many web apps are CRUD (create, read, update, and delete) type apps that all they do is synchronize the front end with remote state data.

In other words, the main issues worth spending time looking into are a range of issues related to remote server caching. Includes how to get, cache, and synchronize server state.

It also includes many other issues, such as handling races, invalidating and reacquiring obsolete data, deduplication, retrying, refetching data when components are refocused, and altering remote data more easily than Redux’s boilerplate code.

Templates for these use cases are unnecessary and overly complex. In particular, middleware such as redux-saga and redux-observable are often required to bind.

In terms of the cost of fetching and changing data from clients, this toolchain is too heavy for these types of applications. And these relatively simple operations are also too complicated.

With the advent of hooks and new contextual APIs. The wind has been moving from using heavy abstractions like Redux to using the native capabilities of the new hooks API for some time. It is usually as simple as using a useContext combined with useState or useReducer.

This is a great way for simple applications. Many small applications can do this. But as applications evolve, this poses two problems:

“Make another Redux.” And it’s easy to get caught up in many of the issues we’ve discussed before. Compared to some libraries dedicated to addressing these particular edge cases, either nothing solves the problem or only a little bit. This has led many to feel the need to advocate the idea that the React context has nothing to do with state management.

“Optimize runtime performance.” Another core issue is optimizing repetitive rendering. When using native contexts, this can be difficult to implement as the app evolves.

It is worth mentioning some modern user-side libraries, such as useContextSelector, designed to help solve this problem. At the same time, the React team is also beginning to consider automatically solving this pain point as part of React in the future.

For most CRUD-type web apps, combining local state with a dedicated remote state management library can help you solve problems well.

Example libraries in this trend include React query, SWR, Apollo, and Relay. and some “renewed” Redux libraries such as the Redux Toolkit and RTK Query.

These are specifically built to solve remote data problems, which are often complex if they are dehandled using Redux alone.

Although these libraries are good abstractions for single-page applications. As far as the Javascript required to fetch and change data is concerned, they still require a lot of overhead. As a community of web builders, the actual cost of Javascript is becoming increasingly important.

It’s worth noting that emerging metaframeworks like Remix have solved this problem. By providing abstraction and declarative mutations to server-first data loading, it no longer needs to introduce a specialized library. It extends the concept of “using the UI as a state function” beyond the client to include back-end remote state data.

For large applications, it is often inevitable that there is a global state share that is different from the remote server state.

We can see that previous state management solutions (such as Redux) were more “top-down” in their implementation. Over time, it tends to absorb all the states at the top of the component tree. The states are all at the top of the tree, and the components below get the state they need through the selector.

In Building Future-Oriented Front-End Architectures, we see the role of bottom-up patterns in building components with composite patterns.

Hooks both provide and promote the principle of combining composable parts together to form a larger whole. The use of hooks marks a shift in the state management approach to giant single global storage. Move towards bottom-up “micro” state management, with an emphasis on consuming smaller state fragments through hooks.

Popular libraries like Recoil and Jotai have validated this bottom-up approach with their concept of “atomic” state.

Atoms are small but complete units of state. They are small pieces of state that can be joined together to form a new derived state. This eventually forms a diagram.

This set of models allows developers to build state step by step in a bottom-up fashion. You can also optimize duplicate rendering by invalidating only the updated atomic state in the diagram.

This is in contrast to subscribing directly to a giant single state and minimizes unnecessary duplication of rendering.

Below is a brief summary of the different approaches taken by each library in New Wave to address core problems in state management. These are the questions we defined at the beginning of the article.

Ability to write and update storage state

“Manual optimization” usually means creating selector functions that subscribe to specific state fragments. The benefit here is that consumers have fine-grained control over how to subscribe and how components that subscribe to that state are re-rendered. One drawback is that this is a manual process, prone to errors, and one might question the need for some unnecessary overhead, which should not be part of the API.

“Auto Optimize” is the process of having the library optimize, which automatically re-renders only the necessary content. The advantage here is of course more convenient, and developers can focus on implementing functionality without having to worry about manual optimization methods. One disadvantage of this is that the optimization process is a black box for developers, and there is no exposure to the exit to manually optimize certain parts, which may feel a little magical.

Memory optimization is often just a problem for large applications. This largely depends on whether the library stores state at the module level or in the React runtime. It also depends on how you construct the storage state.

The benefit of small isolated storage over large monolithic storage is that they can be automatically garbage collected when all subscribed components are uninstalled. Large monomer storage, on the other hand, is more prone to memory leaks without proper memory management.

There is currently no correct answer as to what is the best global state management library. A lot of this question depends on your app’s needs and the people who built it.

But understanding the core issues that state management libraries need to address can help us evaluate the libraries that will emerge now and in the future.

An in-depth understanding of the implementation is beyond the scope of this article. If you’re interested in digging deeper, I recommend Daishi Kato’s React State Management book, which is a great resource that makes a very detailed comparison of some of the newer libraries and methods mentioned in this article.

Garbage Collection in Redux Applications

React without memo

The zombie child problem

useMutableSource -> useSyncExternalStore discussion

Proxy compare


Data flow in Remix

I have formed a particularly good atmosphere of the Node.js community, there are a lot of Node .js friends, if you are interested in Node .js learning (follow-up plans can also be), we can carry out Node .js related communication, learning, co-construction. Below add a koala friend to reply to “Node”.

If you find this article helpful to you, I would like to ask you to do me 2 small favors:

Likes and looks are the biggest support ❤️