Artwork by RJ Barnes
I wrote [flux-standard-functions](https://www.npmjs.com/package/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 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.
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 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 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 definition
or that are defined as immutable will be ignored.
Use [set](https://github.com/skonves/flux-standard-functions/blob/master/src/functions/set.md)
for single values and [setEach](https://github.com/skonves/flux-standard-functions/blob/master/src/functions/set-each.md)
for batch operations.
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](https://github.com/skonves/flux-standard-functions/blob/master/src/functions/patch.md)
for single patches and [patchEach](https://github.com/skonves/flux-standard-functions/blob/master/src/functions/patch-each.md)
for batch operations.
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.
The Standard Functions use the definition
parameter to validate changes. The [define()](https://github.com/skonves/flux-standard-functions/blob/master/src/define.md)
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 tableimmutable()
: The property cannot be changed once setrequired()
: The property must be presentoptional()
: The property may be present or undefined
indexOf(def)
: The property in an “Index” of the provided definitionobjectOf(def)
: The property is a complex objectarrayOf()
: The property is a primitive arrayNote 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.
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.
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 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.
Hopefully, you find the project interesting! If so, here’s how you can help: