• Accidental Complexity When Structuring Your App State
  • This article is authorized by Tal Kol, the original author
  • The Nuggets translation Project
  • Translator: [email protected]
  • Proofread by: Yifili09, DeadLion

The implementation of Redux as a Flux model requires us to think explicitly about the overall state within the application and then spend time modeling it. As it turns out, this is not necessarily an easy task. It is a classic example of chaos theory, in which a seemingly harmless butterfly wing vibrating in the wrong direction can cause a complex chain of effects such as a hurricane. Below is a list of practical tips on how to model application state to make your business logic sound while maintaining usability.


What is application state?

According to Wikipedia – computer programs store data in variables that represent storage locations in the computer’s memory. At any given point in time when the program is executing, the contents of these memory locations are called the state of the program.

For the state we are talking about, it is important to add __ to minimize __ in this definition. When modeling our application for more precise control, we will do our best to express the different states the application can be in with as little data as possible, ignoring the other dynamic variables in the program that can be derived from this core. In a Flux application, the state is stored in a Store object. Different actions are called to modify the state, and the __ view component __ listens for the state change and automatically rerenders it internally.

Redux, as a Flux implementation, adds some additional stricter requirements – such as keeping the entire application’s state in a single store object, which is __ immutable __, and usually __ serializable __.

If you don’t use Redux, the tips below should also be helpful. Even if you don’t use Flux, there’s a good chance they’re useful.

1. Avoid modeling based on server-side responses

Local application state usually comes from the server. When an application is used to display data arriving from a remote server, it is often saved in the same format as the data delivered from the server.

Consider an example of an e-commerce store-management application that a merchant uses to manage store inventory, so displaying a list of products is a key feature. The product list is derived from the server, but the application needs to be kept locally as state for presentation in the view. Let’s assume that the main API that gets the list of products from the server returns the following JSON result:

{ "total": 117, "offset": 0, "products": [ { "id": "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0", "title": "Blue Shirt", "price", {"id": "aec17a8E-4793-4687-9be4-02a6cf305590 ", "title": "Red Hat", "price": 7.99}]}Copy the code

The list of products arrives as an array of objects. Why not store them as an array of objects in the application state?

The design of the server API follows different principles and may not be consistent with the application state structure you want to implement. In this case, the server’s choice of array structure may be related to response paging, breaking up the full list into smaller chunks so that clients can download data as needed and avoid sending the same data multiple times to save bandwidth. They are primarily concerned with network issues, but in general are irrelevant to our application state concerns.

2. Mapping is preferred over arrays

In general, arrays are not convenient for state maintenance. Consider what happens when a particular product needs to be updated or retrieved. This might be the case, for example, if the application provides the ability to edit prices, or if data from the server needs to be refreshed. Iterating through a large array to find a particular product is much more troublesome than querying the product by its ID.

So what’s the recommended approach? Use the mapping type with primary key as the key value as the query object.

This means that the data from the above example can store the state of the application in the following structure:

{ "productsById": { "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0": { "title": "Blue Shirt", "price": 9.99}, "aec17a8E-4793-4687-9be4-02a6cf305590 ": {"title": "Red Hat", "price": 7.99}}Copy the code

What happens if sort order is important? For example, if the order returned from the server is also the order we want to present to the user. For this case, we can store an additional array of ids:

{ "productsById": { "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0": { "title": "Blue Shirt", "price": 9.99}, "aec17a8E-4793-4687-9be4-02a6cf305590 ": {"title": "Red Hat", "price": 7.99}}, "productIds": [ "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0", "aec17a8e-4793-4687-9be4-02a6cf305590" ] }Copy the code

Another interesting point: if we need to display data in the React Native ListView component, this structure actually works fine. This format is needed for the recommended cloneWithRows method that supports stable row ids.

3. Avoid modeling based on the needs of the view

The ultimate goal of application state is to present it to the view and make it enjoyable for the user. It can be tempting to save the state in the form that the view needs, because it avoids extra conversion operations on the data.

Let’s go back to our e-commerce store management example. Assume that every product can be either in stock or out of stock. We can store this data in a Boolean property of the product object.

{"id": "88cd7621-d3E1-42b7-b2b8-8CA82cdAC2f0 ", "title": "Blue Shirt", "price": 9.99, "outOfStock": false}Copy the code

Our application needs to display a list of all out-of-stock products. As mentioned earlier, the React Native ListView component expects to pass two arguments when calling its cloneWithRows method: a mapping of rows and an array of row ids. We tend to prepare this state ahead of time and definitely keep the list. This will allow us to supply two parameters to the ListView without additional conversion. The final structure of the state object is as follows:

{"productsById": {" 88cd7621-d3e1-42b7-b2b8-8ca82cdAC2f0 ": {"title": "Blue Shirt", "price": 9.99, "outOfStock": False}, "aec17a8E-4793-4687-9be4-02a6cf305590 ": {"title": "Red Hat", "price": 7.99, "outOfStock": true } }, "outOfStockProductIds": ["aec17a8e-4793-4687-9be4-02a6cf305590"] }Copy the code

Sounds like a good idea, right? Well, as it turns out, no.

The reason, as before, is that views have their own different concerns. The view doesn’t care about keeping state to a minimum. Specifically, their tendencies are completely opposite, because the data must be laid out for the user. Different views can render the same state data in different ways, and it is often impossible to satisfy them without copying the data.

This brings us to our next point.

4. Avoid storing duplicate data in application state

A good way to test if your state holds duplicate data is to check if you need to update both data points simultaneously to ensure consistency. In the out-of-stock product example above, assume that the first product suddenly becomes out of stock. To process this update, we must change its outOfStock field in the map to true and add its ID to the array outOfStockProductIds – two updates.

Handling duplicate data is simple. All you need to do is delete one of the instances. The reasoning behind this stems from a single source of information: if the data is saved only once, it is no longer possible to achieve an inconsistent state.

If we delete the outOfStockProductIds array, we still need to find a way to prepare this data for use by the view. This transformation must take place at run time before the data is provided to the view. The recommended practice in Redux is to do this in a selector:

{"productsById": {" 88cd7621-d3e1-42b7-b2b8-8ca82cdAC2f0 ": {"title": "Blue Shirt", "price": 9.99, "outOfStock": False}, "aec17a8E-4793-4687-9be4-02a6cf305590 ": {"title": "Red Hat", "price": 7.99, "outOfStock": true } } } // selector function outOfStockProductIds(state) { return _.keys(_.pickBy(state.productsById, (product) => product.outOfStock)); }Copy the code

A selector is a pure function that takes state as input and returns the transformed state we want to consume. Dan Abramov suggests that we place selectors next to reducers because they are usually tightly coupled. We will execute the selector in the mapStateToProps function of the view.

Another possible alternative to deleting an array is to remove the inventory attribute from each product in the map. Using this alternative approach, we can use arrays as a single source of information. In fact, it might be better to change this array to a map following tip # 2:

{ "productsById": { "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0": { "title": "Blue Shirt", "price": }, "aec17a8E-4793-4687-9be4-02a6cf305590 ": {"title": "Red Hat", "price": 7.99}}, "outOfStockProductMap": { "aec17a8e-4793-4687-9be4-02a6cf305590": true } } // selector function outOfStockProductIds(state) { return _.keys(state.outOfStockProductMap); }Copy the code

5. Do not store derived data in state

The single source principle does not apply only to duplicate data. Any derived data that appears in the store violates this principle, as multiple locations must be updated to maintain state consistency.

Let’s add another requirement to our store management example – the ability to put a product on sale and add a discount to its price. The application needs to show the user a filtered list of products, a list of all products, and only products with no discounts or only products with discounts.

A common mistake is to keep three arrays in the store, each containing a list of ids for the related products for each filter. Since the three arrays can be exported from the current filter and product map, it is better to generate them using a selector similar to the one above:

{"productsById": {"88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0": {"title": "Blue Shirt", "price": 9.99, "discount": 1.99}, "aec17a8E-4793-4687-9be4-02a6cf305590 ": {"title": "Red Hat", "price": 0.99, "discount": 0 } } } // selector function filteredProductIds(state, filter) { return _.keys(_.pickBy(state.productsById, (product) => { if (filter == "ALL_PRODUCTS") return true; if (filter == "NO_DISCOUNTS" && product.discount == 0) return true; if (filter == "ONLY_DISCOUNTS" && product.discount > 0) return true; return false; })); }Copy the code

A selector is executed for each state change before the view is rerendered. If your selector is computation-intensive and you are concerned with performance, use Memoization techniques to calculate the results and cache them after a run. You can check out the Reselect component that implements this optimization capability.

6. Normalize nested objects

Overall, the basic motivation for these tips so far is simplicity. States need to be managed all the time, and we want to make this process as easy as possible. Simplicity is easier to maintain when data objects are independent, but what happens when you have interrelationships?

Consider the following example in our store management application. We want to add an order management system where customers can purchase multiple products in a single order. Let’s assume we have a server API that returns the following JSON order list:

{ "total": 1, "offset": 0, "orders": [ { "id": "14e743f8-8fa5-4520-be62-4339551383b5", "customer": "John Smith", "products": [ { "id": "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0", "title": "Blue Shirt", "price": 9.99, "giftWrap": true, "notes": "It's a gift, please remove price tag"}], "totalPrice": 9.99}]}Copy the code

An order contains several products, so we need to model the relationship between the two. We already know from tip # 1 that we should not use the API’s response structure, which does seem problematic because it leads to duplication of production data.

In this case, a good approach is to standardize the data and keep two separate mappings – one for the product and one for the order. Since both types of objects are based on unique ids, we can use the ID attribute to specify the association. The generated application state structure is as follows:

{ "productsById": { "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0": { "title": "Blue Shirt", "price": 9.99}, "aec17a8E-4793-4687-9be4-02a6cf305590 ": {"title": "Red Hat", "price": 7.99}}, "ordersById": { "14e743f8-8fa5-4520-be62-4339551383b5": { "customer": "John Smith", "products": { "88cd7621-d3e1-42b7-b2b8-8ca82cdac2f0": { "giftWrap": true, "notes": "It's a gift, please remove price tag"}}, "totalPrice": 9.99}}}Copy the code

If we want to find all the products that belong to an order, we traverse the keys of the Products property. Each key value is a product ID. Using this ID to access the productsById map will provide us with product details. Additional product details specific to this order, such as giftWrap, are in the values mapped from products under the order.

If the process of standardizing API responses becomes tedious, you can use a helper library, such as Normalizr, that takes a pattern as a parameter and performs the process operations of standardizing data for you.

7. Application state can be treated as an in-memory database

So far, we’ve covered various modeling techniques, and you should be familiar with them.

When modeling a traditional database structure, we avoid duplication and derivation, using primary keys (ids) to map index data in similar tables and normalize relationships between multiple tables. That’s pretty much all we’ve been talking about.

Treating application state like an in-memory database can help put you in the right frame of mind to make better structured decisions.


Treat application state as a first-class citizen

If there’s one thing you’ve taken away from this article, it should be that.

During imperative programming, we tend to think of code as king and spend less time worrying about the “right” model of implicit data structures such as state. Our application state is often found scattered across various managers or controllers as private properties, unbridled organic growth.

However, the situation is different in the declarative paradigm. In environments like React, our system behaves in response to states. Becoming a first-class citizen is as important as the code we write. This is the purpose of the Actions object in Flux and the source of truth for the Flux view.

A tool library like Redux is built on Flux and provides a number of tools, such as introducing immutability to give us better predictability of application state.

We should spend more time thinking about our application state. We should be aware of its complexity and the effort required to maintain it in our code. Just like when we write code, we should refactor it as soon as it shows signs of decay.