Scalable Frontend #3 — The State Layer

Scalable Frontend #3 — The State Layer

Patterns for the single source of truth

State tree, the single source of truth

This post is part of the Scalable Frontend series, you can see the other parts here: “ #1 — Architecture ” and “ #2 — Common Patterns ”.

When dealing with user interfaces, it is mandatory to manage the state to be displayed to or changed by the user, no matter the scale of the application we are working with. The source can be a list fetched from an API, input obtained from the user, data from LocalStorage, and so on. Independently of where this data comes from, we’ll have to deal with it and keep it in sync with a persistence method, be it a remote server or browser storage.

To be precise, this is what we call the local state , a specific portion of data that our app consumes and relies on. There are many reasons for why, when, and where the state can be updated and consumed that it might get out of hand pretty quickly if we don’t manage it properly. Even a simple sign-up form might require juggling with a lot of state:

  • Check if the fields are filled with valid data while the user interacts with it;
  • Skip validation for untouched fields, until the form is submitted;
  • When selecting a country from a dropdown, trigger a request to fetch the states for that country and then cache the response;
  • Change the available options for the languages dropdown based on the selected country.

Whew! Sounds tricky, right?

In this post, we’ll talk about how to manage the local state in a sane way, always keeping in mind the scalability of our codebase and principles of architecture design to avoid coupling between the state layer and other layers. The rest of our application shouldn’t know about the state layer or the library being used if any. We just need to tell the view layer how to fetch data from the state and how to dispatch actions that will call our use cases to compose the behavior of our app.

In the past few years, a lot of libraries to manage the local state emerged in the JavaScript community, previously dominated by two-way data-binding contenders like Backbone, Ember, and Angular. Only with the arrival of Flux and React that one-way data flow became popular, and people realized MVC wouldn’t work well for frontend applications. Along with the large adoption of functional programming techniques in frontend development, we can see why libraries like Redux became so popular and influenced an entire generation of state management libraries.

There is an excellent presentation about the Flux mindset in case you want to learn more about the subject.

Nowadays, there are a couple of popular state management libraries, some of them specific to certain tools — like NgRx is for Angular. We’ll use Redux for familiarity's sake, but all concepts herein are transferrable across libraries or even with no library. With that in mind, you should use what’s best for you and your team. Don’t feel forced to use a library just because it’s the hype. If it works for you, go for it.

The citizens: actions, action creators, reducers and the store

These four are the most common types of objects we’re going to find in state management of modern frontend applications. The idea of using actions to isolate events from their implications and side effects is not new. In fact, these citizens are based on ideas from well-established approaches like event sourcing , CQRS , and the mediator design pattern.

They work together by centralizing the ways of storing and changing the state, by confining it to a single place and dispatching actions (a.k.a. events) to trigger these changes. Once the changes are applied to the state, we notify the parts interested in them, and they update themselves to reflect the new state. This is the one-way data flow cycle.

One-way data flow cycle.

Actions and action creators

Actions are usually implemented as objects with two attributes: a type property and the data to perform the implications of that action by the store. For example, an action to trigger the creation of a user could have the following format:

It’s important to note that the implementation of the type attribute varies depending on the state management library, but most of the time it will be a string. Also, the example action doesn’t create the user by itself; it is just a message that tells the store to create the user with userData .

But what if we need to trigger the same action from more than one place in our code, like a test suite or another file? How can we make it reusable and hide the action type from the unit that is dispatching it? We use action creators! Action creators are functions that abstract the creation of an action object as a reusable unit. Our previous example can be encapsulated by the following action creator:

Now, whenever we need to dispatch the CREATE_USER action, we import this function and use it to create the action object that’ll be dispatched to our store.

The store

The store is the single source of truth of our state. It’s the only place where we store and modify it. Every time we change our state, we dispatch an action to the store describing what change we want to perform along with additional information if needed (respectively, the action type and the user data of our example.) It means we should never mutate our state in the same place we consume it, but leave it up to the store to update it. In most implementations of this pattern, we subscribe to be notified when changes are performed by the store so we can react to them.

The store is the single source of truth of our state.

OK, so now we know the store can be used for two main purposes: dispatch actions and trigger events to listeners. In React applications, it’s common to use Redux to create the store and react-redux’ connect to dispatch actions ( mapDispatchToProps ) and listen to changes ( mapStateToProps ). But we can also have a root component that uses the Context API to store the state, where we would use Context.Consumer to both dispatch actions and listen to changes. Or we can do it in an even simpler way by lifting state up . For Vue, there is a library very similar to Redux called Vuex , where we use dispatch to trigger actions and mapState to listen to the store. Likewise, we can do the same in Angular applications with @ngrx/store .

Even with their differences, all these libraries share the same idea: a unidirectional cycle. Every time we need to update the state, we dispatch actions to the store, which executes them and notifies the listeners. Never the other way around or skipping steps.

Reducers

But how does the store update the state and handle each action type? That’s where reducers come in handy. To be honest, not always we call them “reducers” because in Vuex, for example, it’s called “mutations”. But the central idea is the same: a function that takes the current state of the application and the action being handled, returning a brand new state or mutating the current one with setters. The store delegates updates to this function and then notifies listeners about the new state. And this closes the cycle!

Every reducer should be able to handle any action of our application.

Before finishing this part, there is a very important rule to mention: every reducer should be able to handle any action of our application. In other words, it is OK to have a single action that is handled by more than one reducer at the same time. This rule allows a single action to trigger multiple changes in different parts of the state, for example: after an AJAX request is finished, we can update the local state with the response in reducer X, hide the spinner in reducer Y, and even show a success message in reducer Z, where each of these reducers has the single responsibility of updating a distinct portion of the state.

Designing the State

Some questions that always come to mind when we start writing our application are:

  • What should my state look like?
  • What should I put in it?
  • What shape should it have?

I’m afraid there is no right answer to these questions. The only thing we take for granted are a few basic rules on how the state is updated depending on the library. In Redux, for instance, reducer functions should be pure, deterministic, and have a signature of (state, action) => state .

There are some practices, however, that we can follow to get away from complexity and improve UI performance. Some of them are generic and can be applied to any state management technique. Others are applicable only to tools like Redux, which provides us helper functions with a strong functional accent for splitting reducer logic.

Before digging into that, I recommend checking out the docs of the library you’re using to manage the state. In most cases, you will find advanced techniques and helpers you didn’t know about, and even concepts not covered in this post that are more idiomatic to the state management approach you’re using. Otherwise, you can look into a third-party library, or build functions to achieve that yourself.

State shape

The state refers to the data we need to manage, and the shape refers to how we structure and organize that data. The shape of the state is independent of the source of data but is totally related to how we structure reducer logic.

Usually, the shape is represented with a plain JavaScript object that forms the initial state tree, but it’s also possible to use any other value like plain numbers, arrays, or strings. The advantage of objects, though, is that they allow organizing and dividing the state into meaningful pieces, where each key of the root object is a sub-tree representing a common domain or slice of data. In a basic blog app with articles and authors, the state shape could be like this:

Notice that articles is a top-level key to the state, forming a sub-tree to represent a common concept of data. We also have a nested subtree inside each article to represent authors . As a general rule, we should avoid nested data because it increases the complexity of reducers.

This page from Redux docs goes over how to structure the types of data onto your state shape based on your domain and app state. Go read it even if you are not using Redux! Data management is a common subject for any type of application, and that’s a really good article to learn how to categorize data and organize it to form your state shape.

Combine Reducers

The previous example showed only one key in our state shape, but real-world applications usually have more than one domain to represent — which means more update logic going on into one reducer function. However, that goes against one important rule: reducer functions should be small and focused (the Single Responsibility Principle), as to be easier to read, understand, and maintain.

We can achieve that in Redux with the built-in combineReducers function. As the name implies, this function returns one combined reducer function, and it takes an object where each key represents a sub-tree of the state. For example, we can combine the reducers for authors and articles into a single rootReducer :

Keep in mind that the keys passed to combineReducers will be used in the resulting shape of the state, whose data will be transformed by the reducer functions associated with the keys. So if we’d pass an authors key and an authorsReducer function, the shape returned by rootReducer would be state.authors , managed by the authorsReducer function. The keys passed to rootReducer should never have “reducer” in their names!

Combining reducers is also great because we can go even deeper when splitting our reducer functions. Let’s suppose articlesReducer needs to handle the case where articles are being fetched and keep track of errors that occur during the request. So now the articles key in our state will no longer be an array of articles; it will be an object like this:

We could handle this new situation inside articlesReducer but we’d have even more statements to deal with in a single place. To resolve that, we can split articlesReducer into smaller pieces:

Besides combinerReducers , there are other approaches to split reducer logic but we will not go over them. Redux docs does a great job of describing techniques such as high-order reducers, slicing reducers to make them reusable, and ways to reduce boilerplate code. Note that these approaches can also apply to both VueX modules (which will be mentioned again in this post) and NgRx .

Normalization

Have you noticed that in our blog example, each article has an author nested into it? Well, unfortunately, that leads to duplicated data when an author is associated with more than one article, which makes the act of updating an author a nightmare — because we need to make sure duplicated authors are updated as well. And to make matters worse, performance is degraded due to unnecessary re-renders.

To resolve that, we can treat relational data as if it were a database by resorting to normalization. The technique consists in having one “table” for each data type or domain, where we reference relational entities by their IDs. Redux recommends the following:

byId
allIds

In our example, we would have something like this after normalizing our data:

This structure is much more lightweight. There are no more duplicated items, therefore authors get updated in a single place and fewer UI updates get triggered as a result. Our reducer is simpler and item lookup is easy and consistent.

A common question when starting to normalize our data is:

How to shape those relational portions of data into our state?

While there is no hard rule for this, it’s common to put “tables” of domains inside a top-level object called entities . In our articles example, it would look like this:

And what about data sent by APIs? Because that data is usually sent back in a nested format, it needs to be normalized prior to storing it in the state tree. We can use the Normalizr library to do that, which allows defining schema types and relations to return normalized data in accordance with thereof setup. Go check their docs for more details on its usage.

For people working with smaller applications or not wanting to use a library, it’s easy to implement normalization by hand. We need a few functions for that:

replaceRelationById
extractRelation
byId
allIds

So let’s create those functions:

Pretty straightforward, right? Now we need to call those functions from within the corresponding reducer. Let’s get our first article structure as an example:

After the action is dispatched, we‘ll have a normalized shape to the articles table with only IDs and no nested data , and the authors table will also be normalized without any duplication.

Common Patterns

In the previous post, we discussed patterns pertaining to the domain, application, infrastructure, and input layers. Now let’s go over patterns for keeping the state layer sane and easy to reason about. Some of them are not meant to be used all the time but only in specific situations.

Selectors

Sometimes we’ll need more than just plucking data off the state: we might need to compute derived state with filtering or grouping so that our views get re-rendered only if the derived data changes. For example, if we’re filtering a TODO list by completed items, we don’t need to re-render the view if an incomplete item gets updated, right? Also, computing data directly on the consumer side makes it coupled to the shape of the data, in a way that if we need to restructure the state, we’ll also have to update the code of the consumer, which shouldn’t happen at all. This is exactly the kind of problem we can solve with selectors.

Selectors are functions which, as the name implies, select data that is relevant for a particular context. They receive a portion of the whole state as a parameter and compute it in a way the consumer expects. Let’s get back to our TODO list using React + Redux as an example. What would the code look like before and after using selectors?

We can see that the refactored component has no idea about what kind of TODOs are present within the collection because we extracted this logic out into a selector called getTodosByFilter . This is pretty much what selectors are for, so consider using one when you notice your component knows too much about your state.

Consider using a selector when you notice your component knows too much about your state.

Selectors also give us the possibility of leveraging a performance improvement technique called memoization , which avoids re-rendering and re-computing data as long as the raw data remains intact. With Redux, we can implement memoized selectors using the reselect library, which you can read more about in Redux’ docs .

In case you’re using Vuex, there’s already a built-in way of implementing selectors called getters . You’ll see that the mindset for “getters” is exactly the same as Redux selectors’. NgRx has a selector feature as well, and it even performs memoization for you!

In case you’re wondering about where to put your selectors, keep reading and you’ll soon find out!

Ducks/Modules

Remember when we said architecture is not the same as file organization, but that it would be good that our file organization reflected our architecture? The Ducks pattern is exactly about that: it follows the definition of the Common Closure Principe (CCP) , which says:

The classes in a package should be closed together against the same kinds of changes. A change that affects a package affects all the classes in that package.

— Robert Martin

A duck (or module) is a file where we gather reducer, actions, action creators, and selectors that belong to the same feature, in a way that if we need to change or add a new action, we won’t need to touch more than one file.

But wait a minute, is this pattern specific for Redux applications? Of course not! Even though the name Ducks is inspired by the word Redux , we can follow its mindset for any state management approach we want, even if not using a library.

For Redux users, there’s the documentation for the proposed ducks approach . For Vuex applications, there’s a thing called modules that’s based around the same idea but is even more “native” to Vuex since it’s part of the API’s core library. And in case you’re using Angular with NgRx, there is a proposal based on Ducks called ngrx-ducks .

But there’s a catch. The Ducks approach proposes us to keep action names at the top of the duck file, right? This might not be the best decision because it would make it difficult for reducers from other files to handle any action of our app, as they would be forced to duplicate the action name. We can circumvent this with a separate file for all the action names of our application, that each duck can import and use. This file would group the action names by feature with named exports for each of them. Here’s an example:

State Machines

Sometimes it may be overly complex to manage multiple booleans or multiple conditionals with the variables of our state to find out what should be rendered. A form component may have multiple possibilities to consider:

  • The fields weren’t touched yet, so don’t show the validation messages;
  • The fields were touched and are invalid, so show the validation messages;
  • The fields weren’t touched but the submit button was clicked, so show the validation messages;
  • The fields are valid and the submit button was clicked, so show a spinner and disable the fields;
  • The request succeeded, so hide the spinner and show a confirmation message;
  • The request failed, so hide the spinner, enable the fields, and show the error message.

Can you imagine how many booleans we’d use for that? We would probably have things like this in our code:

We usually end up with code like this when we try to use the data to define what should be rendered, so we add a bunch of boolean variables and try to coordinate them in a sane way — which turns out to be very hard. But what if we try to categorize all those possibilities into some explicit and well-named states? Think about it, our interface will always be in one of the following states:

  • Pristine
  • Valid
  • Invalid
  • Submitting
  • Successful

Notice that from any given state, there are states we can’t transition to. For example, we can’t transition from “Invalid” to “Submitting”, but we can transition from “Invalid” to “Valid” and then to “Submitting”.

This kind of situation is better handled by a computer science concept known as a finite state machine , or a variant thereof created specifically for this situation called statecharts . The idea of a state machine is to define a set of states and the transitions between them.

In our example, the state machine would look like this:

Form edition charts.

It may look complicated, but notice that well-defined states and transitions make our code less complex and easier to add new states in an explicit and concise way. Now our conditionals will only care about the current state, and we’ll no longer deal with complex boolean expressions:

OK, so how do we implement state machines in our code? The first thing to understand is that it doesn’t have to be a complex implementation. We can have a plain string with the name of the current state in our store and update it for each action handled by our reducers. Here’s an example:

But sometimes we’ll need bigger state machines with more states and transitions, or we just want a specific tool for some other reason. For these cases, we can use something like XState . Keep in mind that state machines are agnostic to state management, so we can have them no matter if using Redux, Context API, Vuex, NgRx, or even no library!

There are a few links at the end of this post with more information about state machines and statecharts in case you want to know more about them.

Common Pitfalls

Even when following a good architecture, there are tempting pitfalls to avoid while developing our frontend application. We say tempting because even though they might look harmless, they have great potential to eventually bite our backs. Let’s talk about some don’ts regarding the state layer.

Don’t reuse the same async action for different purposes

Do you recall the first post of the series when we spoke about treating actions in the same way as controllers from backend applications, not having business rules in them and delegating work to use cases? Let’s get back to this subject, but first, let’s define what we mean when we say “actions with side-effects”.

A side-effect happens when the result of some operation affects something outside of its local environment. For our case, let’s consider a side-effect when an action does more than just changing the local state, like sending an AJAX request or persisting data to the LocalStorage. If our application uses Redux Thunk , Redux-Saga , Vuex Actions , NgRx Effects or even a special type of action that performs requests, that’s what we’re referring to.

What makes actions similar to controllers is that both of them have implications . They execute a whole use case and their side-effects, and this is the reason why we don’t reuse controllers and we shouldn’t reuse actions with side-effects as well. When we try to reuse the same action for a different purpose, we also inherit all of its side-effects, which is not desirable because it makes the code harder to reason about. Let’s make it a bit less abstract with an example.

Imagine a loadProducts action that loads a list of products via AJAX and a view that shows a spinner while the request is pending (we’re going to use a Redux Thunk action in our example):

OK, but now we want to reload this list from time to time in order to keep it always updated, so the first impulse would be to reuse this action, right? What if we want updates to happen in the background without showing a spinner? One might argue that it’s possible to add a withSpinner flag for that, so let’s do it:

This is already getting weird since there is some duplication to consider while using the flag, but let’s ignore that for a moment.

Now, what should we do if we want a different action to be triggered for the success case? Pass it as a parameter as well? Can you see that the more we try to make an action generic, the more complex and less focused it gets? How can we solve that and still reuse the action? The best answer is: we don’t .

Resist the urge of reusing actions with side-effects.

For cases like this, resist the urge of reusing actions with side-effects! Their complexity eventually gets unbearable, hard to understand, and hard to test. Instead, try creating two focused actions that leverage the same use case:

Great! Now we can see both actions and understand exactly when each of them should be used.

Note that the same applies when an action with side-effects uses a second action that also has side-effects. We shouldn’t do it because the calling action will inherit all the side-effects from the called one.

Don’t have your views depend on the return of actions

We already know that reusing actions can make our code harder to understand. Now imagine our components relying on the return value of those actions to call side-effects. It doesn’t sound too bad, right?

But this can make our code even harder to understand. Imagine that we are debugging an action to fetch a product. After this action gets called, we realize that a list of comments is fetched for this product but we don’t know where it comes from, and we know for sure it doesn’t come from the action itself. Now it’s getting complicated, isn’t it?

We are treating our actions as controllers, but would we chain calls to controllers in backend applications? I don’t think so.

Never depend on the return of actions to chain promises nor perform any other kind of work. If something should be done after the action call completes, the action itself should handle it.

So as a second rule, never depend on the return of actions to chain promises nor perform any other kind of work. If something should be done after the action call completes, the action itself should handle it — unless it’s a responsibility for another layer, like a redirect (this is actually a responsibility for the view layer, which we're going to discuss in the next article of this series,) your actions should be the entry point of your app, so don’t spread redirect calls all over your components.

Don’t store computed data

Sometimes we have raw data that needs to be transformed into human-readable values, like prices and dates. Let’s say we have a product model and receive something like { name: 'Product Name', price: 14.9 } containing the price as a plain number. Now it’s our job to format this data before showing it off to the user.

So always remember, when a value can be transformed by a pure function, (which means, given the same input, we always have the same output,) we don’t really need to store it into our state; we can just call a transform function in the place where this value will be displayed to the user. In a React view, it would be as simple as <p>{formatPrice(product.price)}</p> .

We often see developers storing the return value of formatPrice(product.price) and this can lead to drawbacks. What happens when we want to send this value back to the server? Or if we need to run calculations with it on the frontend? We would need to transform it back into a plain number, which it’s not ideal and can be totally avoided just by not storing it.

You can argue that calling a function within a render numerous times can affect performance, but we can use techniques like memoization to avoid processing it every time, so performance is not an excuse to not do it. You can use a simple library like mem , or you can abstract this function call into a component like so <FormatPrice>{product.price}</FormatPrice> and use its own React.memo function. But keep in mind that memoization is only really needed when your function requires intensive processing.

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章