Glenn Stovall I help developers become better writers, teachers, and experts. Author of free writing course for devs at https://glennstovall.com/zero-to-10/

Reducing Redux boilerplate with Redux-Leaves

5 min read 1660

How to reduce boilerplate in Redux with Redux-Leaves

Redux is a prime example of a software library that trades one problem for another.

While redux enables you to manage application state globally using the flux pattern, it also leads to filling your application with tedious, boilerplate code.

Even the most straightforward changes require declaring types, actions, and adding another case statement to an already colossal switch statement.

As state and changes continue to increase in complexity, your reducers become more complicated and convoluted.

What if you could remove most of that boilerplate?

Enter: Redux-Leaves

Redux-Leaves is a JavaScript library that provides a new framework for how you handle state changes in your redux application. In a standard redux setup, you have one or maybe a few controllers managing different parts of the application.

Instead, Redux-Leaves treats each node of data, or “leaf” in their nomenclature, as a first-class citizen. Each leaf comes with built-in reducers, so you don’t have to write them.

This enables you to remove a lot of boilerplate from your application.

Let’s compare the two approaches and then look at how to tackle moving from a traditional redux setup to one using Redux-Leaves.

How to get started with Redux-Leaves

Let’s begin by building a simple greenfield application that uses only redux and Redux-Leaves. This way, you can try out the tool before trying to add it to an existing project.

We made a custom demo for .
No really. Click here to check it out.

Then, we’ll look at how you could approach added Redux-Leaves to an existing project. We’ll use create-react-app to set up an environment with a build chain and other tooling quickly.

Starting your project

npx create-react-app my-redux-leaves-demo && cd my-redux-leaves-demo
yarn init
yarn add redux redux-leaves

For this example, we’ll use Twitter as our model. We’ll store a list of tweets and add to it.

Within a store.js file, let’s take a look at at a redux case and compare that to how Redux-Leaves works.

Adding a record: Redux version

Typically, whenever you need to add a new mutation to state, you create:

  • A type constant
  • An action creator function
  • A case in the reducer’s switch statement.

Here’s our redux example that adds a tweet:

Adding a record: Redux-Leaves version

import { createStore } from 'redux'

const initialState = {
  tweets: [],
}

const types = {
  ADD_TWEET: 'ADD_TWEET',
}

const actions = {
  pushTweet: (tweet) => ({
    type: types.ADD_TWEET,
    payload: tweet,
  })
}

const reducer = (state = initialState, action) => {
  switch (action.type) {
    case 'ADD_TWEET':
      return {
        ...state,
        tweets: [
          ...state.tweets,
          action.payload,
        ]
     }
  default:
    return state
  }
}

const store = createStore(reducer)
store.dispatch(actions.pushTweet({ text: 'hello', likes: 0 }))

With Redux-Leaves, there is no need to define a reducer function. The Redux-Leaves initialization function provides a reducer we can pass to createStore.

Also, it provides an actions object that provides action creator functions, so we don’t have to worry about coding those from scratch either.

With all of that taken care of, there is no need to declare type constants. Bye-bye, boilerplate!

Here’s a piece of functionally equivalent code to the above, written with Redux-Leaves:

import { createStore } from 'redux'
import { reduxLeaves } from 'redux-leaves’

const initialState = {
  tweets: [],
}

const [reducer, actions] = reduxLeaves(initialState)
const store = createStore(reducer)

store.dispatch(actions.tweets.create.push({ text: 'hello', likes: 0 }))

It’s much more concise than the previous example. As your requirements grow, the results are more drastic.

In a standard redux application, you have to write new types and expand your reducer for every mutation.

Redux-Leaves handles many cases out-of-the-box, so that isn’t the case.

How do you dispatch those mutations?

With Redux-Leaves built-in action creators. Each piece of data in the state is a leaf. In our example, the tweets array is a leaf.

With objects, leaves can be nested. The tweet itself is considered a leaf, and each subfield of it is also a leaf, and so on. Each has action creators of their own.

An overview of action creators for various data types

Redux-Leaves provides three actions creators for every type of leaf, regardless of type:

  • Update: set the value of a leaf to anything you want
  • Reset: set the value of a leaf back to whatever it was in the initial state
  • Clear: depends on the data type. Numbers become 0. Booleans become false. Strings, arrays, and objects become empty('', [], and {} respectively)

In addition to these, Redux-Leaves provides some additional creators that are type-specific. For example, leaves of the boolean type have on, off, and toggle action creators.

For a complete list, refer to the Redux-Leaves documentation.

Two ways to create actions

You can use the create function directly and dispatch actions that way, or you can declare actions that you can call elsewhere.

The second way maps more closely to how redux currently operates, but also for that reason creates more boilerplate.

I’ll leave it up to you to decide which approach works best for your needs.

// method #1
store.dispatch(actions.tweets.create.push({ text: 'hello', likes: 0 }))

// method #2
const addTweet = actions.tweets.create.push
store.dispatch(addTweet({ text: 'hello', likes: 0 }))

Creating complex actions with bundle

Boilerplate code saves time, but it isn’t able to handle every real-world use case. What if you want to update more than one leaf at a time?

Redux-Leaves provides a bundle function that combines many actions into one.

If you wanted to keep track of the most recent timestamp when you add a tweet, it would look like this:

const updateTweet = (tweet) => bundle([
  actions.most_recent.create.update(Date.now()),
  actions.tweets.create.push(tweet),
], 'UPDATE_WITH_RECENCY_UPDATE')

store.dispatch(updateTweet({ text: 'hello', likes: 0 }))

The first argument is an array of actions to dispatch, and the second is an optional custom type.

But even then, there are probably some cases that this won’t handle either. What if you need more logic in your reducer?

What if you need to reference one part of the state while updating another? For these cases, it’s also possible to code custom leaf reducers.

This extensibility is what makes Redux-Leaves shine: It provides enough built-in functionality to handle simple use cases, and the ability to expand on that functionality when needed.

Creating custom reducer actions with leaf reducers

When tweeting, all a user has to do is type into a text box and hit submit.

They aren’t responsible for providing all of the metadata that goes with it. A better API would be one that only requires a string to create a tweet, and abstract away the actual structure.

This situation is a good use case for a custom leaf reducer.

The core shape of a leaf reducer is the same as with other reducers: it takes in a state and action and returns an updated version of the state.

Where they differ, though, is that a leaf reducer does not relate directly to a single piece of data. Leaf reducers are callable on any leaf in your application.

That’s yet another way Redux-Leaves helps you avoid repetition.

Also note that the state in leaf reducer is not referencing the entire global state — only the leaf it was called on.

In our example, leafState is the tweets array.

If you need to reference the global state, you can pass it in as an optional 3rd argument.

const pushTweet = (leafState, action) => [
  ...leafState,
  {
    text: action.payload,
    likes: 0,
    last_liked: null,
    pinned: false,
  }
]

Add custom leaf reducers to the reduxLeaves function. The key in the object becomes its function signature in the application.

const customReducers = {
  pushTweet: pushTweet,
}

const [reducer, actions] = reduxLeaves(initialState, customReducers)
const store = createStore(reducer)

Then, dispatching actions for custom reducers looks just like the built-in ones:

store.dispatch(actions.tweets.create.pushTweet('Hello, world!'))
console.log('leaves version', store.getState())

Outputs the following:

{
  tweets: [
    { 
      text: “Hello, World!”,
      likes: 0, 
      last_liked: null, 
      pinned: false, 
    }
  ]
}

Migrating to Redux-Leaves

If you are working on an existing project and considering moving Redux-Leaves, you probably don’t want to take the whole thing out at once.

A much safer strategy would be to replace existing redux code one action at a time.

If you have tests in place for your application — which you should before attempting to refactor to a library like this — then this process should be a smooth and easy one.

Replace one action and run the tests. When they pass, repeat.

To do this, I recommend using the reduce-reducers Redux utility. Reduce-reducers enables the combining of existing reducers with new ones.

yarn add reduce-reducers

With this tool, it is possible to add Redux-Leaves to your application, without rewriting any code (yet).

import { createStore } from 'redux'
import { reduxLeaves } from 'redux-leaves'
import reduceReducers from 'reduce-reducers’

Const initialState = {
  // initial state
}

const myOldReducer = (state = initialState, action) => {
  // big case statement goes here
}

const leafReducers = {} // we’ll put custom reducers here if/when we need them

const [reducer, actions] = reduxLeaves(initialState, leafReducers)

const comboReducer = reduceReducers(myOldReducer, reducer) 

const store = createStore(comboReducer)

This update should not change the behavior of your application. The store is updatable by both the old reducers and the new one.

Therefore, you can remove and replace actions one-by-one instead of rewriting everything at once.

Eventually, you’ll be able to make one of those tasty pull requests that make your codebase a few thousand lines shorter without changing functionality.

If you like, this change enables using Redux-Leaves for new code without modifying existing cases.

Conclusion

Removing the complexity of one library by adding another library is a counterintuitive proposition in my book.

On the one hand, you can leverage Redux-Leaves to reduce boilerplate code and increase the speed with which developers can add functionality.

However, adding another library means there is another API developers on the team need to be familiar with.

If you are working alone or on a small team, then the learning curve may not be an issue. Only you and your team can know if redux is the right decision for your project.

Is the reduced codebase and the faster pace of development worth the added dependency and learning required? That’s up to you.

Plug: , a DVR for web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Glenn Stovall I help developers become better writers, teachers, and experts. Author of free writing course for devs at https://glennstovall.com/zero-to-10/

One Reply to “Reducing Redux boilerplate with Redux-Leaves”

Leave a Reply