Scaling Your Redux App with Ducks
Organizing your code for the long term
How does your front-end application scale? How do you make sure that the code you’re writing is maintainable six months from now?
Redux took the world of front-end development by storm in 2015 and established itself as a standard — even beyond the scope of React.
At the company where I work, we recently finished refactoring a fairly large React codebase, adding Redux instead of reflux.
We did it because moving forward would have been impossible without a well-structured application and a good set of rules.
The codebase is more than two years old, and reflux was there from the beginning. We had to change code that hadn’t been touched in more than a year and was pretty tangled with the React components.
Based on the work we did on the project, I put together this repo, explaining our approach in organizing our Redux code.
When you learn about Redux and the roles of actions and reducers, you start with very simple examples. Most tutorials available today don’t go to the next level. But if you’re building something with Redux that’s more complicated than a to-do list, you’ll need a smarter way of scaling your codebase over time.
Someone once said that naming things is one of the hardest jobs in computer science. I couldn’t agree more. But structuring folders and organizing files is a close second.
Let’s explore how we approached code organization in the past.
Function vs. Feature
There are two established approaches of structuring applications: function-first and feature-first.
One the left below, you can see a function-first folder structure. On the right you can see a feature-first approach.
Function-first means that your top-level directories are named after the purpose of the files inside. So you have: containers, components, actions, reducers, etc.
This doesn’t scale at all. As your app grows and you add more features, you add files into the same folders. So you end up having to scroll inside a single folder to find your file.
The problem is also about coupling the folders together. A single flow through your app will probably require files from all folders.
One advantage of this approach is that it isolates — in our case — React from Redux. So if you want to change the state management library, you know which folders you need to touch. If you change the view library, you can keep your Redux folders intact.
Feature-first means that the top-level directories are named after the main features of the app: product, cart, session.
This approach scales much better because each new feature comes with a new folder. But you have no separation between the React components and Redux. Changing one of them in the long run is a very tricky job.
Additionally you have files that do not belong to any feature. You end up with a folder named common or shared because you want to reuse code across many features in your app.
The Best of Two Worlds
Although not in the scope of this article, I want to touch this single idea: always separate State Management files from UI files.
Think about your application in the long run. Imagine what happens with the codebase when you switch from React to another library. Or think how your codebase would use ReactNative in parallel with the web version.
Our approach starts from the need to isolate the React code into a single folder — called views — and the Redux code into a separate folder — called Redux.
This first level split gives us the flexibility to organize the two separate parts of the app completely differently.
Inside the views folder, we prefer a function-first approach in structuring files. This feels very natural in the context of React: pages, layouts, components, enhancers, etc.
To not go crazy with the number of files in a folder, we may have a feature-based split inside each of these folders.
Then, inside the Redux folder…
Enter Re-Ducks
Each feature of the application should map to separate actions and reducers, so it makes sense to go for a feature-first approach.
The original ducks modular approach is a nice simplification for Redux and offers a structured way of adding each new feature in your app.
Yet we wanted to explore a bit what happens when the app scales. We realized that a single file for a feature becomes too cluttered and hard to maintain in the long run.
This is how re-ducks was born. The solution was to split each feature into a duck folder.
A duck folder MUST:
- contain the entire logic for handling only ONE concept in your app, ex: product, cart, session, etc.
- have an
index.jsfile that exports according to the original duck rules. - keep code with similar purpose in the same file, such as reducers, selectors, and actions
- contain the tests related to the duck.
For this example, we haven’t used any abstraction built on top of Redux. When building software, it’s important to start with the smallest amount of abstractions. This way, you make sure that the cost of your abstractions doesn’t outweigh the benefits.
If you need to convince yourself that abstractions can be bad, watch this awesome talk by Cheng Lou.
Let’s see what goes into each file.
Types
The types file contains the names of the actions that you are dispatching in your application. As a good practice, you should try to scope the names based on the feature to which they belong. This helps when debugging more complex applications.
Actions
This file contains all the action creator functions.
Notice how all the actions are represented by functions, even if they are not parametrized. A consistent approach is more than needed in a large codebase.
Operations
To represent chained operations, you need a Redux Middleware to enhance the dispatch function. Some popular examples are: redux-thunk, redux-saga, or redux-observable.
In our case, we use redux-thunk. We want to separate the thunks from the action creators, even with the cost of writing extra code. So we define an operation as a wrapper over actions.
If the operation only dispatches a single action — doesn’t actually use redux-thunk — we forward the action creator function. If the operation uses a thunk, it can dispatch many actions and chain them with promises.
Call them operations, thunks, sagas, epics—it’s your choice. Just find a naming convention and stick with it.
At the end, when we discuss the index, we’ll see that the operations are part of the public interface of the duck. Actions are encapsulated; operations are exposed.
Reducers
If a feature has more facets, you should definitely use multiple reducers to handle different parts of the state shape. Additionally, don’t be afraid to use combineReducers as much as needed. This gives you a lot of flexibility when working with a complex state shape.
In a large scale application, your state tree will be at least three levels deep. Reducer functions should be as small as possible and handle only simple data constructs. The combineReducers utility function is all you need to build a flexible and maintainable state shape.
Check out the complete example project and see how combineReducers is used: once in the reducers.js files and then in the store.js file, where we put together the entire state tree.
Selectors
Together with the operations, the selectors are part of the public interface of a duck. The split between operations and selectors resembles the CQRS pattern.
Selector functions take a slice of the application state and return some data based on that. They never introduce any changes to the application state.
Index
This file specifies what gets exported from the duck folder. It will:
- export as default the reducer function of the duck;
- export as named exports the selectors and the operations;
- export the types if they are needed in other ducks.
Tests
A benefit of using Redux and the ducks structure is that you can write your tests next to the code you are testing.
Testing your Redux code is fairly straightforward:
Inside this file you can write tests for reducers, operations, selectors, etc.
I could write a whole different piece about the benefits of testing your code; there are so many of them. Just do it!
The nice part about re-ducks is that you get to use the same pattern for all your Redux code.
The feature-based split for the Redux code is much more flexible and scalable as your application codebase grows. And the function-based split for views works when you build small components that are shared across the application.
You can check out a full react-redux-example codebase over at GitHub. Just keep in mind that the repo is still under active development.
How do you structure your Redux apps? I’m looking forward to hearing some feedback on this approach I’ve presented.

