Redesigning Redux

February 28, 2018 0 Comments

Redesigning Redux

 

 

Shouldn’t state management be a solved problem by now? Intuitively, developers seem to know a hidden truth: state management seems harder than it needs to be. In this article, we’ll try to answer some questions you’ve probably been asking yourself:

  • Do you really need a library for state management?
  • Is the popularity of Redux deserved? Why or why not?
  • Could we make a better state management solution? If so, how?

Being a front-end developer is not just about moving pixels around; the true art of development is knowing where to store your state. Short answer: it’s complicated, but not that complicated.

Let’s take a look at our options when using a component based view framework/library like React:

State that exists inside of a single component. In React, think state updated with setState.

State passed from a parent to a child. In React, think props passed as properties on a child component.

State held in a root provider, and accessed by a consumer somewhere down the component tree, regardless of proximity. In React, think of the context API.

A lot of state belongs in the view, as it reflects the UI. But what about all the other code that reflects your underlying data and logic?

Putting everything inside of views can lead to a poor separation of concerns: it ties you to a javascript view library, it makes code harder to test, and perhaps the greatest annoyance: you have to constantly think and readjust where you store your state.

State management is complicated by the fact that designs change and it’s often hard to tell which components will need which state. The simplest choice is to just provide all of the state from the root component, at which point you’re probably better off just going with next option.

State can be moved outside of your view library. The library can then “connect” using the provider/consumer pattern to keep in sync.

Perhaps the most popular state management library is Redux. Over the past two years it has grown massively in popularity. So why so much love for a simple library?

Is Redux more performant? No. In fact, it gets slightly slower with each new action that must be handled.

Is Redux simpler? Certainly not.

Simple would be pure javascript:

So why isn’t everyone just using global.state = {}?

Under the hood, Redux really is just the same as TJ’s root object — only wrapped within a pipeline of utilities.

The Redux Store Pipeline

In Redux, you can’t directly modify the state. There is only one way in: dispatch an action into the pipeline that eventually updates the state.

There are two sets of listeners along the pipeline: middleware & subscriptions. Middleware are functions that can listen to actions passed in, enabling tools such as a “logger”, “devtools”, or a “syncWithServer” listener. Subscriptions are the functions used to broadcast these state changes.

Finally, reducers update functions that can break down state changes into smaller, more modular and manageable chunks.

Redux may actually be simpler for development than having a global object as your state.

Think of Redux as a global object with pre/post update hooks, and a simplified way of “reducing” the next state.

Yes. There are several undeniable signs of an API in need of improvement; these can be summed up with the following equation:

Consider timesaved to represent the time you may have spent developing your own solution, while timeinvested equates to the hours invested in reading documentation, taking tutorials, and researching unfamiliar concepts.

Redux is essentially a simple and small library with a steep learning curve. For every developer that has overcome and benefitted from Redux as a deep dive into functional programming, there has been another potential developer lost and thinking “this isn’t for me, I’m going back to jQuery”.

You don’t need to understand what a “comonad” is to use jQuery, and you shouldn’t necessarily need to comprehend functional composition to handle state management.

The purpose of any library is to make something more complicated seem simple through abstraction.

To be clear my intent is not to troll Dan Abramov. Redux became too popular, too early in its infancy.

  • How do you refactor a library already used by millions of developers?
  • How can you justify publishing breaking changes that effect countless projects around the world?

You can’t. But you can provide amazing support through extensive docs, educational videos and community outreach. Dan Abramov for the win here.

Or perhaps there’s another way.

I would argue that Redux deserves a rewrite. And I come armed with 7 areas that should be improved.

Let’s take a look at a basic setup from the real world Redux example on the left.

Many developers have paused here, after just the first step, staring blankly into the abyss. What’s a thunk? compose? Can a function even do that?

Consider if Redux were based on configuration over composition. Setup might look more like the example on the right.

Reducers in Redux could use a switch away from the unnecessarily verbose switch statements we’ve grown used to.

Assuming that a reducer is matching on action type, we can invert the params so that each reducer is a pure function accepting state and an action. Maybe even simpler, we could standardize actions and pass in only state and a payload.

Thunks are commonly used for creating async actions in Redux. In many ways, the way a thunk works seems more like a clever hack than an officially recommended solution. Follow me here:

  1. You dispatch an action, which is actually a function rather than the expected object.
  2. Thunk middleware checks every action to see if it is a function.
  3. If so, the middleware calls the function and passes in access to some store methods: dispatch and getState.

Really? Is it not bad practice for a simple action to be a dynamically typed as an object, or function, or even a Promise?

Like the example on the right, can’t we just async/await?

When you think about it, there are really two kinds of actions:

  1. Reducer action: triggers a reducer and changes state.
  2. Effect action: triggers an async action. This might call a Reducer action, but async functions do not directly change any state.

Making a distinction between these two types of actions would be more helpful and less confusing that the above usage with “thunks”.

Why is it standard practice to treat action creators and reducers differently? Can one exist without the other? Does changing one not effect the other?

Action creators and reducers are two sides of the same coin.

const ACTIONONE = 'ACTIONONE' is a redundant side effect of the separation of action creators and reducers. Treat the two as one, and there is no more need for large files of exported type strings.

Grouping the elements of Redux by their use, and you’re likely to come up with a simpler pattern.

Its possible to automagically determine the action creator from the reducer. After all, in this scenario the reducer can become the action creator.

Use a basic naming convention, and the following is predictable:

  1. If a reducer has a name of “increment”, then the type is “increment”. Even better, let’s namespace it: “count/increment”.
  2. Every action passes data through a “payload” key.

Now from count.increment we can generate the action creator from just the reducer.

These pain points are the reason we created Rematch.

Rematch is a wrapper around Redux that provides a simpler API, without losing any of the configurability.

Rematch: The Redux Framework

See a complete Rematch example below:

I’ve been using Rematch in production for the past few months. As a testimonial, I’ll say:

I have never spent so little time thinking about state management.

Redux isn’t going away, and shouldn’t. Embrace the simple patterns behind Redux with less of learning curve, less boilerplate and less cognitive overhead.

Try Rematch, see if you don’t love it. 
And give us a star to let others know you do.


Tag cloud