How I Eliminated Redux Boilerplate with Hooks-for-Redux
Redux is great. It lets you persist, restore, log, and investigate your application’s state in a reliable, predictable way. Further, there are lots of great libraries out there like react-redux, redux-devtools and redux-logger which make working with Redux a pleasure… almost.
There’s just one thing. Redux takes an excessive amount of boilerplate to code. The standard way to use Redux is a highly manual, redundant process of dispatching, routing and reducing your state. But it is just that: standard — and that means automatable.
After working on several projects using Redux, and noticing these patterns, I started designing a tool to automate it. I’m not the first person to observe this: reduce-react-boilerplate, reducing-boilerplate-in-redux and minimize-redux-boilerplate-with-4-lines-of-code, nor the first to try to address this: reduxless, redux-actions, redux-arc and many more. However, these previous attempts each fail in one of two ways:
- They offer a non-standard solution that doesn’t integrate well with the Redux ecosystem
- Or they fall short of removing all the redundancy of the standard Redux pattern.
So I created hooks-for-redux. I liked the way hooks cleaned up React, so I decided to use a similar API to clean up Redux. I’ll explain how H4R works with an example:
How I Used H4R to Eliminate 52% of the Code in the React-Hooks-Todo App
I recently read Sunil Sandhu’s great post detailing how well React-Hooks cleans up how you use Redux in components. I‘m going to use his result as my starting point:
- how-i-reduced-my-redux-code-using-redux-hooks (github)
For those who don’t want to wait, you can see the final results here:
Essential Logic of the To-Do App
Before we dive into the code it is useful to understand the essential logic of the app. An app can be no simpler than its essential logic. That gives us a target to strive for as we attempt to clean up and reduce the code size of the To-Do app.
Starting with the user-interface, the React components pseudo-code might look like this:
As you can see, there are just two components. ToDo consists primarily of a list of ToDoItems, one for each item from useList. The ToDo component also has a text-box and a button for adding new ToDo items via addItem. ToDoItems show each item’s text and a deleteItem button.
This psuedo-code is almost functional except addItem and deleteItem aren’t getting passed the correct parameters.
The rest of the application is managing the todo list itself. Here’s the pseudo-code:
At the top of the pseudo-code you can see the structure of the list and its initial state. Lines 7 and 8 describe the logic behind our two list operations: addItem and deleteItem. These are expressed as reducers — they take the current list’s state, plus an item, and return a new list with the change applied.
Last, there are three stubs for the three functions used in the components above:
- useList returns the current value of the list and re-renders the component it is used in whenever the list changes
- deleteItem deletes the item from the list
- addItem adds the item to the list
The entire, near-working essential logic of the ToDo app is just 30 lines of code. Real apps have more code to manage styling and robustness, but the goal is to get as close to this minimal form as possible.
Let’s Get Started
I’m going to take it one file at a time starting with the root index.js. I’ll show the before and after and explain what changed.
I’m using lines to measure code size. Though it’s not my preferred method, it is easiest. More on my thoughts on counting code in tokens here.
src/index.js (22% less lines)
This is the root file for the application.
index.js before (14 lines):
index.js after (11 lines):
You’ll notice two changes to src/index.js. First, there is no call to configureStore, and second, there is a new Provider which doesn’t require a store parameter. Hooks-for-redux is designed to add reducers to an existing store rather than require all reducers be defined before the store is created. This allows us to use a default store, which is sufficient for many applications. That way you don’t have to explicitly create it or bind to it throughout your application. Of course, you can override the default store as needed.
src/App.js (67% less lines)
App is the root React Component for the application.
App.js before (15 lines):
App.js after (5 lines):
Simplifying App.js is pretty straight forward. App.js shouldn’t be dependent on the List state at all. Among other problems, that unnecessary dependency causes App to re-render whenever the list changes, which is wasteful. Only ToDo needs to re-render in that case. In general, always minimize dependencies between modules.
src/components/ToDo.js (42% less lines)
ToDo.js before: (72 lines)
ToDo.js after: (42 lines)
React code cleanup:
- I’m using export-const, not export-default — I generally avoid export-default. I thought it would save code, which undoubtedly is the point, but I’ve found it actually tends to increase code-length and fragility. One problem with export-default is when you import a default you can assign any name you want, and that opens up the possibility of inconsistent naming in a project. However, my biggest problem with export-default is it doesn’t work with “import *” or “export *” — which are very powerful constructs for reducing code redundancy.
- I inlined a few of the shorter functions. I find that while adding new, named things can help with clarity, excessive naming can also hurt readability. In general, I try to not name things that will only ever be used once. That way you can read them in-place where they are used and know exactly what’s going on. If you do name something, it’s important for it to add clarity. Names like ‘handleInput’ tell you nothing about what the function does while significantly decreasing readability. A better name would have been ‘captureTextUpdate’.
There are more substantial changes as well:
- I imported addItem and useList. As noted in the App.js changes, they shouldn’t be passed in as props, so now they are imported directly.
- I removed generateId. It is not the ToDo component’s responsibility to create and guarantee unique ids. It’s the responsibility of the list model.
- I removed deleteItem. ToDo.js never uses deleteItem, so it shouldn’t be dependent on it.
src/components/ToDoItem.js (35% less lines)
These changes are pretty straight forward:
ToDoItem.js before: (17 lines)
ToDoItem.js after: (11 lines)
- deleteItem is imported directly rather than coming from props
- export-const (same as ToDo.js)
- props-destructuring directly in the argument list allows us to make the entire render function an expression
redux/list.js (74% less lines, 80% less files!)
Now for the real magic. H4R’s useRedux eliminates all the boilerplate needed to manage your Redux store, and it does this without sacrificing any power or compatibility. It allows you to put all your Redux logic in one file.
Note, on larger projects I recommend creating one file per data-model. In this simple app, though, there is only one data-model: list.
5 redux-related files before: (61 lines)
Just list.js after: (16 lines)
getUniqueId is basically the same as generateId originally in ToDo.js. I moved it out of the React-view and into the Redux-model where it belongs.
The rest of the file is the call to useRedux. Each call to useRedux defines a slice of named Redux state. It takes three main arguments:
- storeKey, a string, is the property name where your state will be stored in the Redux store.
- initialState can be anything
- reducers is an object of named reducers
(the names are the action names used during dispatch)
And it returns, ready to use, an array containing the following:
- A React hook for getting the current state and re-rendering the component whenever the state changes.
- An object of named dispatchers matching the provided reducers.
Note: There are some additional inputs and outputs not shone in this example. Refer to the README for more.
This is how hooks-for-redux works. It takes the essential information needed to define your state — name, initial state and reducers, and it returns the essential methods for interacting with that state. Internally it’s doing exactly what you’d be doing manually yourself — in a clean, efficient and most importantly, well-tested way.
Finished! How Did We Do?
How did we do compared to the 30-line essential solution? If you merge the H4R version into one-file you can remove most of the import and export lines. If you further remove all the styling, it slims down to just 45 lines. Compared to the essential, not-quite-working solution, the extra 15 lines account for handling text input, initializing the React app and a few other details required to make it actually work. I don’t think you can make it much simpler:
Hooks-for-Redux and Redux
Redux is a great extensible, client-side state management platform. I want to give plenty of credit to Dan Abramov and Andrew Clark. Redux’s extensibility is what makes H4R possible.
However, Redux falls short from the application-author point of view. The standard pattern for using Redux is highly repetitive. Not only does it take more time to write the same code over and over, but it also creates more surface area for bugs to occur. The standard pattern litters dependencies all over your code and encourages mixing your Redux logic with your React components.
Almost all good software engineering comes down to one thing: Don’t Repeat Yourself. This is what hooks-for-redux does for you. It eliminates the repetition without sacrificing any power. You still define your state. You still define your reducers, but the rest is taken care of for you.
Best of all, H4R is small. The core function, useRedux, is about 10 lines of code, and the entire library is just 90. That means, even with a tiny project like this, not only is your source code smaller and more robust, but your total code-size is also smaller. The savings only grow as your project grows.
What to Read Next
I have two follow-up articles to this original H4R announcement. I dive deeper into the good software engineering, modular design, and how to write better Redux with or without H4R.
The 5 Essential Elements of Modular Software Design
Complexity kills developer productivity. Thankfully, modular design gives us complexity-fighting superpowers.
Modular Redux — a Design Pattern for Mastering Scalable, Shared State
How to write React in 2020 with shared state that accelerates development, take half the code and scales to very large…
Resources and Acknowledgments
- Get started with hooks-for-redux on npmjs.org
- Learn more about how hooks-for-redux works
- input source:
- output source:
- Thank you Sunil Sandhu for encouraging me to write this Medium post.
- Hooks-for-redux was developed at GenUI.co. I love that GenUI values contributing back to open source, and I’m grateful for the opportunity to create H4R. Thank you to my colleagues at GenUI who helped test and refine H4R.