Redux Advanced Series 2: how to reasonably design the state of Redux

Time:2021-8-24

Redux is a very popular state management solution. Any time during the execution of a Redux application is a reflection of the state. It can be said that state drives the operation of Redux logic. Designing a good state is not easy. This paper first introduces the two most common mistakes in designing a state, and then leads to how to design a state reasonably.

Error 1: API based state design

An API is often used as the basis for designing a state. An API corresponds to a sub state, and the structure of the state is consistent (or nearly consistent) with the data structure returned by the API. For example, a blog application,/postsThe interface returns the blog list. The returned data structure is as follows:

[
  {
    "id": 1,
    "title": "Blog Title",
    "create_time": "2017-01-10T23:07:43.248Z",
    "author": {
      "id": 81,
      "name": "Mr Shelby"
    }
  }
  ...
]

We also need to view the details of a blog, assuming through the interface/posts/{id}Get blog details through the interface/posts/{id}/commentsGet blog comments. The returned data structure is as follows:

{
    "id": 1,
    "title": "Blog Title",
    "create_time": "2017-01-10T23:07:43.248Z",
    "author": {
      "id": 81,
      "name": "Mr Shelby"
    },
    "content": "Some really short blog content. "
}
[
  {
    "id": 41,
    "author": "Jack",
    "create_time": "2017-01-11T23:07:43.248Z",
    "content": "Good article!"
  }
  ...
]

The data of the above three interfaces are regarded as three sub states to form the global application state:

{
  "posts": [
    {
      "id": 1,
      "title": "Blog Title",
      "create_time": "2017-01-10T23:07:43.248Z",
      "author": {
        "id": 81,
        "name": "Mr Shelby"
      }
    },
    ...
  ],
  "currentPost": {
    "id": 1,
    "title": "Blog Title",
    "create_time": "2017-01-10T23:07:43.248Z",
    "author": {
      "id": 81,
      "name": "Mr Shelby"
    },
    "content": "Some really short blog content. "
  },
  "currentComments": [
    {
      "id": 1,
      "author": "Jack",
      "create_time": "2017-01-11T23:07:43.248Z",
      "content": "Good article!"
    },
    ...
  ]
}

In this state, there are many duplicate information between posts and currentpost, and posts and currentstatements are array type structures, which are not easy to find. Each time you find a record, you need to traverse the entire array. These problems are essentially because the API is designed based on the logic of the server, not based on the state of the application. For example, although the basic information such as the title and author of each blog has been obtained when obtaining the blog list, for the API for obtaining blog details, according to the design principles of the API, this API should still contain these basic information of the blog, not just return the content of the blog. For another example, the reason why posts and currentstatements return array structures is to consider factors such as data order and paging.

Error 2: design state based on page UI

Since you can’t design a state according to the API, many people will go to another opposite and design a state based on the page UI. The state is designed according to the data and data format required by the page UI. Taking todo application as an example, the page will have three states: display all items, obviously all to-do items and display all to-do items. If the state is designed based on the page UI, the state will be as follows:

{
  "all": [
    {
      "id": 1,
      "text": "todo 1",
      "completed": false
    },
    {
      "id": 2,
      "text": "todo 2",
      "completed": true
    }
  ],
  "uncompleted": [
    {
      "id": 1,
      "text": "todo 1",
      "completed": false
    }
  ],
  "completed": [
    {
      "id": 2,
      "text": "todo 2",
      "completed": false
    }
  ]
}

This state is very convenient for components that display the UI. The UI is rendered with the array type data of the corresponding state in which the current application is in, without any intermediate data conversion. However, the problems of this state can also be easily found. First, this state still has the problem of data duplication; Second, when adding or modifying a record, more than one place needs to be modified. For example, when a new record is added, both the all and incomplete arrays need to add the new record. This type of state will not only waste storage, but also run the risk of data inconsistency.

These two design methods of state are actually two extreme design methods. In actual projects, few developers design state completely according to these two methods, but most people will be affected by these two design methods. Please recall, have you ever used the data returned by an API as part of a state intact? Have you ever defined a state for the UI of a component for the convenience of component rendering?

Rational design state

Let’s take a look at how to reasonably design a state. The most important and core principle is to design a state like a database. The state is regarded as a database, each part of the state in the state is regarded as a table in the database, and each field in the state corresponds to a field of the table. When designing a database, the following three principles should be followed:

  1. Data is classified by domain and stored in different tables. Column data stored in different tables cannot be duplicated.
  2. The data of each column in the table depends on the primary key of the table.
  3. Columns other than the primary key in the table cannot have direct dependencies on each other.

These three principles can be translated into the principles when designing a state:

  1. The state of the whole application is divided into several sub states according to the domain, and duplicate data cannot be saved between the sub states.
  2. State stores data in the structure of key value pairs, takes the record key / ID as the record index, and other fields in the record depend on the index.
  3. Data that can be calculated from existing data cannot be saved in the state, that is, the fields in the state do not depend on each other.

According to these three principles, we redesign the state of blog application. By domain, a state can be divided into three sub states: posts, comments and authors. The records in Posts take the ID of the blog as the key value, including title and create_ Time, author and comments. The structure of comments and authors can be designed in the same way. The final state structure is as follows:

{
  "posts": {
    "1": {
      "id": 1,
      "title": "Blog Title",
      "content": "Some really short blog content.",
      "created_at": "2016-01-11T23:07:43.248Z",
      "author": 81,
      "comments": [
        352
      ]
    },
    ...
  },
  "comments": {
    "352": {
      "id": 352,
      "content": "Good article!",
      "author": 41
    },
    ...
  },
  "authors": {
    "41": {
      "id": 41,
      "name": "Jack"
    },
    "81": {
      "id": 81,
      "name": "Mr Shelby"
    },
    ...
  }
}

Now does the state look like a database with three tables? However, this state does not meet the application requirements: the storage method of key value pairs can not ensure the order of blog list data, but for blog list, order is obviously required. To solve this problem, we can define another status postids to store the blog ID in array format:

{
  "posts": {
    "1": {
      "id": 1,
      "title": "Blog Title",
      "content": "Some really short blog content.",
      "created_at": "2016-01-11T23:07:43.248Z",
      "author": 81,
      "comments": [
        352
      ]
    },
    ...
  },
  "postIds": [1, ...],
  "comments": {
    "352": {
      "id": 352,
      "content": "Good article!",
      "author": 41
    },
    ...
  },
  "authors": {
    "41": {
      "id": 41,
      "name": "Jack"
    },
    "81": {
      "id": 81,
      "name": "Mr Shelby"
    },
    ...
  }
}

In this way, when the blog list is displayed, the list order is obtained according to postids, and then the blog information is obtained from posts according to the blog ID. Some students in this place may have doubts and think that both posts and postids save ID data, which violates the principle that there can be no duplicate data between different states. But in fact, this is not duplicate data. The data saved by postids is the order of the blog list, but the “order” data is reflected by the blog ID. This is the same reason that the primary key of one table can be used as the foreign key of another table at the same time. It should also be noted that when a new blog is added, the two states of Posts and postid need to be modified. This seems to become troublesome. It is not as simple as directly using an array type of state. However, when it is necessary to modify the data of a blog, this structure has obvious advantages. Moreover, if you directly use an array to save the state, there will be the problem of too deep object nesting level. Imagine that you need to access the content of comments through similar methodsposts[0].comments[0].contentThe three-tier structure can be obtained. When the business is more complex, this problem becomes more prominent. A flat state has better flexibility and scalability.

So far, our states have been designed based on the domain data returned by the background API, but in fact, the application’s state needs to include not only the domain data, but also the application’s UI logic data. For example, it handles the page loading effect according to whether it is currently communicating with the server; When the application runs in error, it needs to display error information, etc. At this time, the structure of the state is as follows:

{
  "isFetching": false,
  "error": "",
  "posts": {
    ...
  },
  "postIds": [1, ...],
  "comments": {
    ...
  },
  "authors": {
    ...
  }
}

With the increase of application business logic, there will be more and more nodes at the first level of state. At this time, we often consider merging node data with strong correlation, and then split the reducer to let each child reducer process the state logic of a node. In this example, we can merge posts and postids, adjust the state name, and merge isfetching and error as global UI logical states:

{
  "app":{
    "isFetching": false,
      "error": "",
  },
  "posts":{
    "byId": {
      "1": {
        ...
      },
      ...
    },
    "allIds": [1, ...],
  } 
  "comments": {
    ...
  },
  "authors": {
    ...
  }
}

In this way, we can define four reducers: appreducer, postsreducer, commentsreducer and authorsreducer to process four sub states respectively. So far, the structural design of state has been completed.

To sum up, the key to designing a Redux state is to design the state like a database. State is regarded as a database applied in memory, and action and reducer are regarded as SQL statements operating the database.


Welcome to my official account: the front end of veteran cadres, and receive 21 books from the front end.

Redux Advanced Series 2: how to reasonably design the state of Redux