Intro to side-effects with Redux
By: Mike Yoon
Tags: JavaScript, State Management, Redux

An opinionated guide to Javascript state management — Part 3: Side-effects

Introducing side-effects and a few example approaches to model them in Redux along with their trade-offs

Photo by Chris Ried on Unsplash

In the previous article we covered essential and derived state, but now it’s time to talk about how that state comes into the application. So in this article, I will give an introduction to side-effects, the interactions that affect state outside our application. In addition to providing examples, we’ll discuss trade-offs in different implementations.

What are effects?

There is a very formal definition of a side-effect, but maybe some examples would be easier to digest. A really common one in-browser applications would be making HTTP calls to external web services. Such a call can result in new information being brought in directly (e.g. a GET request), or mutating outside data. Logging to the console, or opening a modal are also side-effects as well, since they result in some change outside your code (browser console and DOM, respectively). Whether or not your application is tracking the results, it’s critical to properly manage the life-cycle for these effects to avoid bugs.

How are they be modeled?

The various state management frameworks try to help using different approaches, each with their own trade-offs. Regardless of the framework, the underlying goal is always the same: provide an API to allow for the capture of as much state around an effect as necessary so the UI can react appropriately. Being a somewhat vague goal, frameworks can choose to accomplish it quite differently.

In my opinion, how a framework chooses to manage effects primarily depends on one aspect: how easily the relevant state can be retrieved. For example, in Redux, the state can be viewed via getState , but most of time you don’t want to read the entire state tree. That means effect logic is likely going to have a step to parse the relevant parts of the state before executing an API call, for instance (demonstrated in examples below). Contrast that with MobX, where an action in a model (equivalent to a subset of a Redux store) can just directly reference its model’s properties. More complex than either would be NgRx, which only allows reading via observable, which in turn allows for an enormous amount of flexibility in effect design. Simpler architectures allow for more intuitive patterns, but there are trade-offs I will discuss later.

Naive Redux example

Out of the box, the base Redux package does not provide any framework for managing asynchronous actions, so you can choose to roll your own patterns. Most folks will probably use the standard redux-thunk library, which provides documentation for this very case. Below is an example of making a login request to a REST API. For educational purposes, I won’t use any extra libraries in this simple example.

The doLogin function encapsulates all the logic for the effect:

Dispatching the login action to mark the start of the effect. Making the api.login() call. Dispatching either a loginFailed or loginSuccess action based on the result of the promise.

An alternative approach

Handling side-effects is a part of any UI, but frameworks can differ quite a bit in their approaches. In Redux you have to get the current state, and then dispatch success/failure actions. In some other frameworks, you can actually just handle it more intuitively. See this example from the MobX documentation:

I haven’t covered MobX in much detail and if you aren’t familiar, just focus on the fetchProjects function. Aside from the runInAction calls necessary to invoke MobX’s dirty-checking, it probably looks like something you might write if you had no framework at all. It allows us to just read and write the relevant state like any other JS object, which makes for a simpler flow.

This doesn’t mean that MobX is objectively “better”, it’s just a different approach that could be a good fit for your application. We’ll see some trade-offs of simpler approaches later in this article. Hopefully, these examples will help you recognize effects and give you some idea of how to model them in your code. Now let's look at another kind of effect, synchronous effects.

Synchronous effects

Effects aren’t limited to just IO calls or other asynchronous operations. Logging to the console, opening/closing a modal, or executing a DOM operation all result in a change to the external state. This doesn’t mean you have to handle these types of effects in the same way, just know that it’s an option that may be more maintainable. In a lot of cases, just keeping things simple would be fine. For example, there’s nothing inherently wrong with code like this:

In most simple cases this is actually optimal, but if you need to have multiple components all capable of opening the same modal, it could end up a bit messy. Let’s look at how that would play out in some different implementations.

Naive Redux implementation

To improve this, you could refactor the common handleClick code into a new function, which might be enough, but let’s take it another step further.

Redux-thunk implementation

Another option is to create a previously mentioned async action using redux-thunk and dispatch that from both components.

This requires two actions to get the job done, but is still relatively easy to follow. The multiple dispatches in each component is a bit messy, so we could refactor FooAction and BarAction to both be async actions that could do that for us. Redux doesn’t try to push you towards any specific implementation. Let’s check out another framework that has patterns for this specific case.

Redux-saga implementation

redux-saga has a different take on effects, using an event-listener type of pattern called a saga (in simple terms, an asynchronous function that can be invoked in response to one or more actions, but with all sorts of options). Using that, we can setup a handler that listens for both FooAction and BarAction and reacts appropriately.

The big difference now is that by reading only the component code, you would not know that a modal would be opened. You’d have to scan through the sagas (which would almost certainly be in another file) to know that.

Trade-offs

You’ve got a lot of choices when it comes to modeling your side-effects and it may seem confusing as to why you need three or more ways to do the same thing. It mainly boils down how tightly-coupled you want the source of your action (the component in this case) to be to the result of the action (opening the modal). The first of the previous three examples above would be very tightly coupled, while the last redux-saga version would be very loosely coupled.

Benefits of tight coupling

Can easily read the effect logic either directly in the component or only one jump away

Simple to add or remove logic to or from the function (i.e. all the logic is right in front of you, so usually no surprises if you change it)

Downsides

Difficult to control if or when an effect happens. For example, if you wanted to limit the modal from opening based on some state, it’d be trivial to do so in a saga, but more involved in other implementations based on how tightly coupled they are.

Also more work to inject extra asynchronous steps into the flow (e.g. instead of FooAction immediately opening the modal, show a confirm button that, once clicked, shows the modal instead).

immediately opening the modal, show a confirm button that, once clicked, shows the modal instead). Possibly more difficult to unit test the individual pieces of logic

Loose coupling has roughly the opposite trade-offs, and the degree to which depends on the specific implementation. In an ideal world, you would try to be as tightly coupled as you can get away with because of looser coupling results in more boilerplate and complexity. If you need the abstraction then you will be very happy to have it, but otherwise, it’s just extra work. Unfortunately, knowing how to evaluate exactly what you need is a skill that can probably only be developed through experience. If you are new, somewhere in the middle is a safe place to start, and redux-thunk async actions would be a good option for Redux users. I wouldn’t recommend considering a tool like redux-saga unless you have a very good understanding of Redux.

Final Words

Before wrapping up this article, I’d like to briefly talk about a great paper called “Out of the tar pit”. It’s an academic paper from 2006 that discusses, in great detail, why software complexity is the root cause of most software development problems. I highly recommend reading through it if you’re interested in deeply understanding the theory behind what makes software engineering complicated. It’s a bit of a long read, but it’s very well-regarded and has influenced me into going deeper into state management, so I think it’s definitely worth your time.

At this time, I strongly believe state management has one of, if not the largest impact on UI maintainability and reliability. So if you’re somewhat new to this space, I highly encourage you to continue learning as much as you can, on the job or otherwise. If you have written multiple applications already, I’m certain you’ve spent a lot of time wrangling state, whether or not you were using a framework like Redux. There’s always room for improvement, so be sure to keep an eye out for unnecessary difficulties.

This wraps up my introductory series about front-end state management. Thank you for taking the time to read them and I hope I was able to spark some deeper interest in state management. In my future articles, I’ll cover my own solutions or approaches to complex situations based on my personal experience, so stay on the lookout for that.