And how you too can build simple, predictable reducers with Flux Standard Functions
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 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 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.
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.
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.
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
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
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.
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.
“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.
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.
_.merge functions map roughly to the
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 is a small, but powerful utility for taking JSON with a schema definition and returning nested entities with their IDs, gathered in dictionaries.”
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.”
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:
- Weird behavior? Confusing docs? Github issues are super helpful! 😃
- Give it a Star: https://github.com/skonves/flux-standard-functions ⭐
- Give this post a few 👏