Steve Konves

@stevekonves

How I replaced Redux with Redux

Artwork by RJ Barnes

And how you too can build simple, predictable reducers with Flux Standard Functions

TL;DR

I wrote flux-standard-functions which is a tiny (2.1kb) package for building simple, predictable reducers for Redux. It helps make awesome things easy, like replacing reducer boilerplate with clear, validated, atomic mutations. It works great with both existing and greenfield projects. ⭐ Give it a star!

Redux is Hard

Redux is the library that everyone loves to hate. Hardly does a day go by that I don’t run across some article claiming that “library X” is killing Redux. Why is Redux “hard” and why are developers so bent on “killing” it?

Fundamentally, Redux exists to resolve the tension between “two concepts that are very hard for the human mind to reason about: mutation and asynchronicity.” It does so through a tradeoff with another potentially difficult concept: indirection. Such a tradeoff when approached correctly can yield really good results. However, if you think Redux is a silver bullet, you’re gonna have a bad time.

“Redux is a tradeoff” — Dan Abramov

But besides the complexity of indirection, Redux has another difficulty: its simplicity. One of the best features of Redux its lack of them. At the time of writing, the library weighs in at only 2.6kb when minified and gzipped. Its small size is due to is lack of features and opinions. This simplicity makes Redux very flexible.

But ultimate flexibility can often be a barrier to productivity. Andrew Clark introduced a “human-friendly standard for Flux action objects” which he calls “Flux Standard Actions” (or FSAs) to rein in the flexibility of actions. He observed that “It’s much easier to work with Flux actions if we can make certain assumptions about their shape.” The opinions introduced by FSAs help the developer reason about the shape of actions, however, there has still been a lack of similar standards for building reducers.

Much of the boilerplate of Redux reducers comes from the ubiquitous bespoke spreading of objects to create new states. Flux Standard Functions (FSFs) are a set of specific functions that can be composed to form reducers. By removing some of Redux’s ultimate flexibility, FSFs allow developers to build and understand simple, predictable reducers.

When faced with a challenge like Redux, developers have a few options: improve the experience or “kill it” and use something else. In general, I find the Redux tradeoff worth it for large projects. So after looking through a ton of Redux reducers looking for common patterns, I think I have landed on a solution that is simple, comprehensive, productive, and backward compatible.

Show me some code!

Here is an example of a “Todo reducer” that is based on an example provided in the Redux docs:

Here is the same example implemented with Flux Standard Functions:

First, let’s take a look at the most obvious differences. To start with, the structure of a Todo (todoDef) is defined outside of the reducer. Doing so helps to prevent duplication as we add more Todo action types. Next, the resulting reducing function is really small!

Second, we are actually getting a few things for free that weren’t present in the original example. If id was undefined, the original reducer would create an invalid new state. In the “FSF” example, if the Todo id is undefined, the set function determines that the Todo is invalid and returns the original state by reference. This validation not only ensures that the application state stays valid, but also adds the performance optimization of not returning a new state object if nothing changes.

For a more apples-to-apples comparison (including validation), the original hand-written reducer would look more like this:

The above example starts to show how even a very simple operation can start to bloat over time. All we did was add validation and suddenly we have the same Todo properties defined in multiple locations. As this reducing function evolves over time and as new action types are added, it will become difficult to ensure that the definition and validation of a Todo item remain consistent throughout the Todo reducer.

This is just one example of simplifying a fairly trivial operation (adding a new item to state). Let’s take a look at the Three Functions to understand how they can be used to build pretty much any reducer you can think of.

The Three Functions

The Standard Functions work with a combination of three parameters: target, payload, and definition. The target is the data that is being "mutated". (Note: that if the target is changed, then a shallow clone is created.) The payload is the new data that is being added, updated, replaced, or removed. The definition is an object that describes the structure of the target object and is used for validation, indexing, and optimization.

Set

Set provides the ability to either add or overwrite data. This is analogous to the “Create” CRUD operation. If a value that is being set already exists, then it will be overwritten. If the value being set does not exist then it is added. Any operations that set a value not included in the definitionor that are defined as immutable will be ignored.

Use set for single values and setEach for batch operations.

Patch

Patch provides the ability to update (or “upsert”) data. This is similar to the “Update” CRUD operation. If a value being patched already exists, then it will be replaced. For complex properties, it will be partially updated with the properties in the payload. If the property did not already exist and is valid per the definition then it will be added.

Use patch for single patches and patchEach for batch operations.

Unset

Unset provides the ability to remove data. This is analogous to the “Delete” CRUD operation. If the valued being unset exists, then it is removed. If the value being unset does not exist or is specified by the definition to be required or immutable, then nothing happens.

Use unset to remove single values and unsetEach for batch unset operations.

Definitions and Rules

The Standard Functions use the definition parameter to validate changes. The define()function is used to create the definition for the types of objects in the application state.

Here is an example of defining a “User” type:

The following rules can be used to create type definitions:

  • key(): Property is the “key” of an “Index” or table
  • immutable(): The property cannot be changed once set
  • required(): The property must be present
  • optional(): The property may be present or undefined
  • indexOf(def): The property in an “Index” of the provided definition
  • objectOf(def): The property is a complex object
  • arrayOf(): The property is a primitive array

Note that Typescript allows developers to define types as well; however, Typescript types are a compile-time-only construct. Once the code as been transpiled down to vanilla Javascript, the type definitions themselves disappear. This means that they cannot be used at runtime for validation. Because of this, the define() function is designed to work independently of or in conjunction Typescript. If you are using Typescript, you are provided with rich type- and property-checking. If you are not using Typescript, then you still benefit from the runtime validation provided by the Standard Functions.

Prior Art

As with most new libraries, Flux Standard Functions stands on the shoulders of giants. Several projects have influenced the development of this one (or at least take a stab at solving a similar problem). I would be remiss to not mention them here.

Flux Standard Actions

“A human-friendly standard for Flux action objects. It’s much easier to work with Flux actions if we can make certain assumptions about their shape.” The Standard Functions are based on a similar philosophy that reducers are easy to build if we can make assumptions about their composition.

Redux Data Normalization

The principles of Data Normalization discussed in the Redux docs heavily influenced this project. Like the docs recommend, Flux Standard Functions work well with a “flat” or normalized state. The concept of “tables” loosely translates to “Indexes” in this project. I, too, recommend keeping state flat. YMMV.

Underscore/Lodash

The _.set and _.merge functions map roughly to the set and patch Standard Functions. There are a number of examples to be found online of using Underscore or Lodash for building reducers. This project provides a small subset of lodash-like functionality optimized for use with Redux.

Normalizr

“Normalizr is a small, but powerful utility for taking JSON with a schema definition and returning nested entities with their IDs, gathered in dictionaries.”

Immer

Immer is the hot new library that became popular during the development of Flux Standard Functions and is definitely worth a look. It is “a tiny package that allows you to work with immutable state in a more convenient way. It is based on the copy-on-write mechanism.”

Angular 1

See this conference talk for more info.

Like what you see?

Hopefully, you find the project interesting! If so, here’s how you can help:

  1. Weird behavior? Confusing docs? Github issues are super helpful! 😃
  2. Give it a Star: https://github.com/skonves/flux-standard-functions
  3. Give this post a few 👏

More by Steve Konves

Topics of interest

More Related Stories