background

Why think about state management design

In recent years, with the development of industrial Internet, more and more applications choose to implement in the browser side. Browsers are also opening up more and more features.

However, as browsers become more and more powerful, the front end becomes onerous. The state repository needs to store more and more things. As a simple front-end monitoring system, it involves error display, data report, error filtering query and so on. Many of these data are in intersection. Once our data acquisition is in intersection, it means that there are the following problems:

  1. Two copies of one type of data are available, and the data is redundant
  2. Unnecessary data is obtained, causing unnecessary server stress
  3. Not a good combination

Of course, that’s the problem with intersection. This will also lead to a single piece of data impure, unable to achieve a high level of abstraction and versatility. Over time, this kind of management will store more and more mixed data models, resulting in management trouble.

Think of state weaving in back-end terms

Therefore, we are keen to make the management model of the data more abstract, so that it can be flexibly assembled and used in any business scenario. This is similar to the concept of “pure functions” in functional programming:

Pure function + pure function = pure function

Let’s turn to the back end. Suppose that the back-end interface for error monitoring is going to give us an error capture message, what about the back-end data query logic?

The following figure shows a joint check implementation of the two tables. One issue table and one error table. In the backend database design, issue and Error are associated, often by referring to each other’s IDS. This allows us to decouple the two tables and then assemble them when we need to combine queries.

As you can see, thanks to the multi-table lookup of many databases, the back end can easily take the desired data from multiple tables, assemble it, and return it through the interface.

State normalization

Based on the above considerations, we can adopt the state paradigm scheme. Before we use a stereotype, let’s understand what it is.

According to the official redux documentation (redux.js.org/usage/struc…) :

Each type of data gets its own “table” in the state tree. Each type of data gets its own “table” in the state tree.

Each “data table” should store the individual items in an object, With the IDs of the items as keys and the items themselves as the values. The ID of the item is used as the key and itself as the value.

Any references to individual items should be done by storing the item’s ID.

Arrays of IDs should be used to indicate the ordering of data.

To put it simply, it is to change our data from three-dimensional to flat, and further manage the data model that can be abstrused independently. The connection model between data uses ID for reference connection search, which can accelerate the speed of data search. Such as:

This access to find the way, similar to the database of multiple tables. So in many cases, we expect the formalized model of the front end to correspond to the model of the database. We can abstract our current state from the model based on the concept of the stereotype. The data is extracted according to the model, and then associative references are made according to the query relationship

After the abstraction is complete, our solution for finding data in the business also needs federated queries. Thus, the complexity of our query is reduced from O(N) to O(1). The query performance is greatly improved

normalizr.js

Of course, such data assembly makes reading faster, but it also complicates the implementation of the separation of source data. Here we can use normalizr.js, which is officially recommended by Redux. It can quickly peel off our data according to the pre-set data model, making our data transformation easier.

With a simple data model definition, we can separate the data according to the model. As shown in the above example

import { normalize, schema } from 'normalizr';

// Define a users schema
const user = new schema.Entity('users');

// Define your comments schema
const comment = new schema.Entity('comments', {
  commenter: user
});

// Define your article
const article = new schema.Entity('articles', {
  author: user,
  comments: [comment]
});

const normalizedData = normalize(originalData, article);
Copy the code

The annoyance of repeated rendering

Of course, redux’s natural state management solution has a huge performance problem — it needs to be managed by a common component. This implementation often results in unnecessary components regenerating the component tree. For example, we have an error monitoring system when we get the latest list of error messages: although our error message entries have increased, the type of error has not changed. But using only the wrong type of component still triggers a re-render. We certainly don’t want this to be the case, because when you’re dealing with more complex calculations, unnecessary repeat rendering can have a big impact on performance.

useSelector

Of course, we can use the React-Redux useSelector hook to filter the required states. The useSelector itself has multiple levels of caching to ensure that components are triggered only when the data in use is updated without unnecessary component updates.

As you can see from the source code, each time the action is submitted, the equalityFn function is executed to compare the result of this selector execution with the result of the last one. If so, return directly. Does not trigger the logic to repeat the render later

But there are still flaws in this approach. After each action is committed, the selector function of the useSelctor is still regenerated, although the component is not regenerated (although there is reselect, caching is also a cost). It is recommended that a useSelctor only return a single non-reference type field value at a time, otherwise a shallow comparison will cause the component to rerender again.

Recoil

Concept & Advantages

Recoil is Facebook’s React-based (currently experimental) status management framework. The big advantage is that it can accurately trigger only the components that render state updates based on orthogonal diggings, all of which are subscription-based. Based on subscription, it also avoids the problem of useSelector selectors that need to be regenerated with each status update.

As you can see below, instead of redux a global state tree, Recoil recommends breaking the state into pieces that are shared only with the components used.

In Recoil, there are two core concepts: Atom and Selector. Atom is the smallest unit of state. When Atom is updated, subscribed components are triggered to update as well. If multiple components subscribe to the same Atom, they share this state. You can simply think of Atom as the smallest data source in Recoil

const fontSizeState = atom({
  key: 'fontSizeState'.default: 14});Copy the code

And the point of selector is to use it with Atom. Selectors can add custom getters and setters to Atom. When Atom changes, the selector that subscribed to it also changes and is render again by the component that subscribed to the selector

const fontSizeLabelState = selector({
  key: 'fontSizeLabelState'.get: ({get}) = > {
    const fontSize = get(fontSizeState);
    const unit = 'px';
    return `${fontSize}${unit}`; }});Copy the code

Of course recoil also supports read/write granularity inconsistencies in state. For example, my state contains two properties, a and B. When I read the state, I read only the a property. Then the component that uses only the B property will not change.

This is a huge performance boost, and to some extent indirectly avoids recoil’s state splitting problem

Cooperate with Suspense

The best thing about Recoil, of course, is that state reading supports asynchronous functions. And synchronous and asynchronous can be mixed, synchronous functions can also accept asynchronous read values. Of course, this point will have the greatest advantage in Suspense.

Take the following code for example. The state GET that I define in the selector is asynchronous, but it’s synchronous when I use it in my component. This is insensitive to the user.

Of course, Suspense works better because we don’t need other states to judge whether the asynchronous calculation has got the data.

const currentUserIDState = atom({
  key: 'CurrentUserID'.default: 1});const currentUserNameQuery = selector({
  key: 'CurrentUserName'.get: async ({get}) => {
    const response = await myDBQuery({
      userID: get(currentUserIDState),
    });
    returnresponse.name; }});function CurrentUserInfo() {
  const userName = useRecoilValue(currentUserNameQuery);
  return <div>{userName}</div>;
}

function MyApp() {
  return (
    <RecoilRoot>
      <React.Suspense fallback={<div>Loading...</div>} ><CurrentUserInfo />
      </React.Suspense>
    </RecoilRoot>
  );
}
Copy the code

conclusion

  • Rethinking state design with back-end state models
  • Abstract the model as much as possible to ensure the purity of individual data. Convenient for flexible assembly in business
  • A state design that is too flexible may result in unnecessary rerendering of components. You need to control the granularity
  • Be aware of the performance cost of unnecessary repetitions. Strategies such as caching can be used to avoid repeated renderings