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?
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.
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.
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.
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.
Typically, whenever you need to add a new mutation to state, you create:
Here’s our redux example that adds a tweet:
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.
Redux-Leaves provides three actions creators for every type of leaf, regardless of type:
''
, []
, 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.
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 }))
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.
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, } ] }
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.
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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowes-toolkit is a lightweight, efficient JavaScript utility library, ideal as a modern Lodash alternative for smaller bundles.
The use cases for the ResizeObserver API may not be immediately obvious, so let’s take a look at a few practical examples.
Automate code comments using VS Code, Ollama, and Node.js.
Learn to build scalable micro-frontend applications using React, discussing their advantages over monolithic frontend applications.
One Reply to "Reducing Redux boilerplate with Redux-Leaves"
Or you can move to Hookstate – faster and simpler to use alternative to Redux. https://hookstate.js.org/ (disclaimer: I am am maintainer for the project)