Documente Academic
Documente Profesional
Documente Cultură
Nir Kaufman
This book is for sale at http://leanpub.com/thinking-in-Redux
This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing
process. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools and
many iterations to get reader feedback, pivot until you have the right book and build traction once
you do.
Thinking in Redux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Construct, Dispatch, Process . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Action in, Action out . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Getting Started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Utilities Middleware . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
The Logger Middleware . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
The Action Splitter Middleware . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
Utilities Middleware Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
Reducer Enhancers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
Definition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
CONTENTS
Selectors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
Feature Selectors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
Query Selectors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
Memoized Selectors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
Selectors Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
Book Code
You can find the code examples used in this book in this GitHub repository: github.com/thinking-
in-redux¹ Note the different branches. Additional resources are listed in the appendix.
Contact Details
You are welcome to contribute your opinions as pull requests to the book’s code repository —
feedback is always welcome.
You can also contact me via the book’s page on leanpub.com/thinking-in-Redux²
Nir Kaufman
¹https://github.com/thinking-in-redux
²https://leanpub.com/thinking-in-Redux
Thinking in Redux
Redux is more than just another state management library for JavaScript; it is a set of tools that help
us implement a lean version of a frontend-oriented messaging system. If you are building a single-
page application with a lot of asynchronous operations that constantly change the state, you should
consider adopting Redux as a solution. This book is an opinionated guide to Redux that focuses on
design patterns, conventions, and practices.
• An action is dispatched from the view with the help of a factory function called an action
creator.
• The action passes through one or more middleware.
• The middleware does any necessary processing of the action and passes it to a reducer
• The view queries parts of the state using selectors in order to render the result to the screen.
• The action creator, responsible for action construction. This returns an action object.
• The middleware, responsible for processing the action and passing it along the chain to a
reducer.
• The reducer, responsible for transferring data from the action payload to the state without any
further processing.
So, it all comes down to the middleware. Every action in the system passes through the middleware
before hitting the reducer. I logically divide middleware into two categories:
Getting Started
Now that we’ve covered some basic concepts and terms, the rest of this book will focus on action
construction and processing patterns. Because Redux (both the library and the design patterns
Thinking in Redux 4
around it) is framework-agnostic, this book won’t include any user interface rendering examples
at all. You can apply the knowledge gained here to any popular frontend framework out there
(Angular, React, Vue, etc.). While the code examples are written in pure JavaScript, you can find
links to popular supporting libraries that can reduce boilerplate code in the appendix of this book.
Programming with Actions
Working with Redux means programming with actions. If we want to change our application state,
we dispatch an action instead of calling a function and expecting it to return something. Therefore,
we need to acquire a set of design patterns that fit this programming model. The first step will be to
learn about action construction, which we will discuss in detail in this chapter.
Action Intentions
An action is nothing more than a plain JavaScript object that bundles data together. The difference
between one action and another is the intention of the action’s sender. In Redux, actions can be
dispatched from the user interface or from a middleware.
Identifying and naming actions by their intention will help you in the design process and implemen-
tation phase of your flow. We can categorize actions in Redux into three groups by their intention:
• Command actions
• Event actions
• Document actions
Each type of action plays an important role in the system and helps us design, implement, and debug
our flow efficiently. Let’s look at them each in turn.
Command Actions
You can think of a command action as an API call to the server. This type of action is the starting
point of a process that will trigger other types of actions. A command action will never be processed
by a reducer. In most cases these actions are dispatched from the user interface, but they can be
dispatched from middleware as well. A command action will carry raw data as its payload, and
optional metadata that we can think of as a set of instructions for the middleware. When creating
a command action we use verbs that emphasize the intention of the action, like GET, FIND, REMOVE,
etc. The following is an example of a typical command action:
Programming with Actions 6
1 const FETCH_BOOKS = {
2 type: 'FETCH_BOOKS',
3 payload: {
4 query: 'redux',
5 },
6 meta: {
7 timeout: 3000
8 }
9 }
This action starts a series of events that involve fetching a collection of books. The timeout value on
the action’s meta property indicates the amount of time that we want to dedicate for this operation
to complete. Note that the command action does not know or care how the books will be fetched.
Events
An event is an action that notifies the system about the beginning, progress, or ending of a procedure.
Event actions usually act as extension points that allow other action processors to hook into the flow.
These actions also play an important part in the debugging process, by enabling us to track the data
flow straight from the logs. Event actions are processed only by middleware. Like command actions,
they don’t carry the final data that needs to be written to the state. The following example shows a
typical event:
Event action example
1 const API_REQUEST_PENDING = {
2 type: 'API_REQUEST_PENDING',
3 }
4
5 const ROUTING_STARTED = {
6 type: 'ROUTING_STARTED',
7 payload: {
8 userId: '123456'
9 },
10 meta: {
11 from: '/home',
12 to: '/dashboard'
13 }
14 }
Document Actions
A document action carries a primitive data type (such as a Boolean) or a data structure (such as
an array) to be written to the state. Therefore, a document action is processed only by a reducer.
The payload of a document action contains the final shape of the data that needs to be a part of
the application state, without further manipulation required. It’s a good practice to match the type
name of the document action with the actual data operation. The following example shows a typical
document action:
Document action example
1 const SET_BOOKS = {
2 type: 'SET_BOOKS',
3 payload: [{...},{...}]
4 }
5
6 const UPDATE_BOOKS = {
7 type: 'UPDATE_BOOKS',
8 payload: [{...},{...}]
9 }
10
11 const REMOVE_BOOK = {
12 type: 'REMOVE_BOOK',
13 payload: 2545852
14 }
This table can help you decide where you should process your actions.
Programming with Actions 8
1 const API_SUCCESS = {
2 type: 'API_SUCCESS',
3 }
The action type name is descriptive; this is clearly an event action that notifies about a successful
API call to the server. But we can’t understand what triggered this event or what kind of data is
related to this successful call just by looking at the action type name in the debugger. Consider a
typical use case where multiple API requests are being sent and successfully returned. In this case,
the action log will look like this:
Action log
1 > 'API_SUCCESS'
2 > 'API_SUCCESS'
3 > 'API_SUCCESS'
4 > 'API_SUCCESS'
5 > 'API_SUCCESS'
We will have to dig deeper into the action payload or metadata to understand which features are
related to these successful responses. A better approach would be to prefix the type name with a
matching feature name, or any other keyword that helps us understand the context. We call this an
integrity key. It helps us to relate different actions that are dispatched at different times to specific
features in our application. Consider the following example of a prefixed action type name:
Prefixed event action
1 '[Books] API_SUCCESS'
This event action notifies about a successful API call that relates to our Books feature. Adopting this
approach makes it much easier to understand and track the flow in the debugger. The following
example shows an action log with prefixed events in different contexts:
Programming with Actions 9
1 const API_REQUEST = {
2 type: 'API_REQUEST',
3 payload: 'redux'
4 meta: {
5 method: 'GET',
6 url: '/books',
7 timeout: 3000,
8 feature: 'Books'
9 }
10 }
By including a rich metadata object, we ensure that the middleware responsible for processing API
actions can be generic and easy to implement. Don’t try to keep your metadata lean and minimal.
The more information the action provides, the better.
Action Construction
We use simple factory functions as action creators. Those functions are responsible for constructing
and returning action objects. Because action creators are not action processors, they should not
perform any procedures that involve any other logic besides creating an object (no loops or data
Programming with Actions 10
manipulation). Also, the action creator should not be aware of other parts of the system. The only
reason for creating action factories is to keep our code clean, without repetition (DRY). I find it
helpful to keep the action type constants close to their action creators (in the same file). The following
example shows a typical action creator for an API command action:
API command action creator
The most important thing to remember is that an action creator must conform to these rules:
Thinking in Actions
Let’s apply what we just learned about actions by implementing a common asynchronous flow. We
will revisit this flow throughout the rest of this book, adding more complexity and interest. The
requirements are as follows:
Although it is a simple flow, a lot is going on. I recommend the following workflow.
Looking at this, it should be clear which actions should be processed in a middleware (the command
and event actions) and which should be processed in a reducer (the document actions). It is also
clear how many actions we need to construct. Let’s create a file for each action type and declare the
action type names as constants:
Programming with Actions 12
actions/books.js
1 // feature name
2 export const BOOKS = '[Books]';
3
4 // action types
5 export const FETCH_BOOKS = `${BOOKS} FETCH`;
6 export const SET_BOOKS = `${BOOKS} SET`;
actions/api.js
1 // action types
2 export const API_REQUEST = 'API_REQUEST';
3 export const API_SUCCESS = 'API_SUCCESS';
4 export const API_ERROR = 'API_ERROR';
actions/ui.js
1 // action types
2 export const SET_LOADER = 'SET_LOADER';
actions/notification.js
1 // action types
2 export const SET_NOTIFICATION = 'SET_NOTIFICATION';
I use square brackets to prefix my action type names with the related feature names, but you should
choose whatever works for you. Always remember that action type names are used for debugging
and monitoring your flow. Note that I defined prefixes for the books command and document actions
while leaving the API, UI, and notification actions without any prefix. The reason for this is that
those actions are generic and can be triggered in different contexts. We will add prefixes for those
actions at construction time in the next step.
actions/books.js
1 // feature name
2 export const BOOKS = '[Books]';
3
4 // action types
5 export const FETCH_BOOKS = `${BOOKS} FETCH`;
6 export const SET_BOOKS = `${BOOKS} SET`;
7
8 action creators
9 export const setBooks = ({books}) => ({
10 type: SET_BOOKS,
11 payload: books
12 });
For the API command action and event actions as well as the notification and UI document actions
we will dynamically pass a feature name so we can use it as a prefix. This will keep our action type
constants “clean” and enable us to reuse those actions across our system:
actions/api.js
1 // action types
2 export const API_REQUEST = 'API_REQUEST';
3 export const API_SUCCESS = 'API_SUCCESS';
4 export const API_ERROR = 'API_ERROR';
5
6 // action creators
7 export const apiRequest = ({body, method, url, feature}) => ({
8 type: `${feature} ${API_REQUEST}`,
9 payload: body,
10 meta: {method, url, feature}
11 });
12
13 export const apiSuccess = ({response, feature}) => ({
14 type: `${feature} ${API_SUCCESS}`,
Programming with Actions 14
15 payload: response,
16 meta: {feature}
17 });
18
19 export const apiError = ({error, feature}) => ({
20 type: `${feature} ${API_ERROR}`,
21 payload: error,
22 meta: {feature}
23 });
actions/ui.js
1 // action types
2 export const SET_LOADER = 'SET_LOADER';
3
4 // action creators
5 export const setLoader = ({state, feature}) => ({
6 type: `${feature} ${SET_LOADER}`,
7 payload: state,
8 meta: {feature}
9 });
actions/notification.js
1 // action types
2 export const SET_NOTIFICATION = 'SET_NOTIFICATION';
3
4 // action creators
5 export const setNotification = ({message, feature}) => ({
6 type: `${feature} ${SET_NOTIFICATION}`,
7 payload: message,
8 meta: {feature}
9 });
reducers/books.js
reducers/ui.js
reducers/notification.js
Actions Summary
Dividing our actions into categories by intention helps us to design and implement our flow fast.
Furthermore, because reducers only care about document actions, we can implement them at a very
early stage. By prefixing our actions we create a meaningful log that describes our flow and helps
us monitor and debug our system. Now that we have both ends of our flow in place (request and
documents), it’s time to implement our middleware.
Action Routing Patterns
In this and the following chapter we will get familiar with common patterns and terms for action
processing. Adopting this common language will make it easier for us to reason about and describe
our flows to others.
Action processing patterns can be divided into two main categories:
Both routing and transforming can depend on the action content, the application context, neither of
these, or both. In this chapter we will focus on common routing patterns. We’ll look at transforming
patterns in the next chapter.
Core Middleware
These are responsible for processing generic actions. The core middleware should not be aware of
any entities or other kind of business logic related to data models and therefore can be reused in
different contexts and even in other applications. The core middleware never depend on any other
middleware.
Feature Middleware
These are responsible for implementing a specific flow. In most cases, a feature middleware will
implement action routing patterns related to a specific feature without transforming the payload in
any way. It cannot be reused in any other context. Like core middleware, the feature middleware
never depend on other middleware.
Action Routing Patterns 18
Feature middleware
In the rest of this chapter we will implement both core middleware and feature middleware.
middleware/books.js
1 import {FETCH_BOOKS} from "../actions/books";
2
3 export const booksMiddleware = () => (next) => (action) => {
4 next(action);
5
6 switch (action.type) {
7
8 case FETCH_BOOKS:
9 // do something
10 break;
11 }
12 };
We are calling next in the first line of our middleware body, before the action filtering (the switch
statement). We do this to keep our action log in order. When a middleware accepts one action
and dispatches another action without calling next with the input action, we refer it as “action
swallowing.” In most cases we will want to see the original action in the Redux log.
Next, we want to split the command action into two actions for further processing:
middleware/books.js
1 import { FETCH_BOOKS } from "../actions/books";
2 import { apiRequest } from "../actions/api";
3 import { setLoader } from "../actions/ui";
4
5 const BOOKS_URL = 'https://www.googleapis.com/books/v1/volumes?q=redux';
6
7 export const booksMiddleware = () => (next) => (action) => {
8 next(action);
9
10 switch (action.type) {
11
12 case FETCH_BOOKS:
13 next(apiRequest({body: null, method: 'GET', url: BOOKS_URL, feature: BOOKS}));
14 next(setLoader({state: true, feature: BOOKS}));
15 break;
16 }
17 };
Action Routing Patterns 20
Moving forward, we care about two event actions that will be dispatched as a result of the API_-
REQUEST command action: [Books] API_SUCCESS and [Books] API_ERROR. In both cases, we will split
the event action into two document actions:
middleware/books.js
Because feature middleware “talks” only with core middleware, I always put the core middleware
at the end of the middleware chain and call next from the feature middleware to fire an action.
Action Routing Patterns 21
The API middleware dispatches a different event as a result of processing the command action. We
consider this mapping. In our case, we want to make a call to the server and dispatch an event on
success or in the case of an error.
Let’s implement this behavior:
middleware/api.js
store.js
1 import {DevTools} from '../ui/DevTool'
2 import {createStore, combineReducers, applyMiddleware, compose} from 'redux';
3 import { booksReducer } from './reducers/books.reducer';
4 import { booksMiddleware } from './middleware/books';
5 import { apiMiddleware } from './middleware/api';
6 import {uiReducer} from "./reducers/ui.reducer";
7 import {notificationsReducer} from "./reducers/notification.reducer";
8
9 // shape the state structure
10 const rootReducer = combineReducers({
11 books: booksReducer,
12 ui: uiReducer,
13 notification: notificationsReducer
14 });
15
16 // create the feature middleware array
17 const featureMiddleware = [
18 booksMiddleware
19 ];
20
21 // create the core middleware array
22 const coreMiddleware = [
23 apiMiddleware
24 ];
25
26 // compose the middleware with additional (optional) enhancers,
27 // DevTools.instrument() will enable dev tools integration
28 const enhancer = compose(
29 applyMiddleware(...featureMiddleware, ...coreMiddleware),
30 DevTools.instrument()
31 );
32
33 // create and configure the store
34 export const store = createStore( rootReducer, {}, enhancer );
We will also add a new event action to notify the application about the normalize operation. The
reason for this is that the normalize middleware processes and dispatches the same document action
(SET_BOOKS). We want to know if the payload was transformed.
So, let’s create a DATA_NORMALIZED event action:
actions/data.js
Note that for this middleware, we are not calling next on the first line. The reason for this is that
this middleware accepts and dispatches the same action. Calling next in this case would cause a
duplication in the action log.
To keep this example focused on the pattern, we will use the normalizeKey passed by the action
sender (the books middleware) for converting an Array into an Object.
The next step is to dispatch the NORMALIZE_DATA event action just before the actual transformation
takes place. This is the final version of our normalize middleware:
normalize.middleware.js
17
18 // fire the books document action
19 next({...action, payload: books, normalizeKey: null });
20
21 } else {
22 next(action);
23 }
24 };
Now we can pass a key when dispatching the SET_BOOKS action from the books middleware to trigger
the transformation:
books.middleware.js
1 ...
2 case `${BOOKS} ${API_SUCCESS}`:
3 next(setBooks({books: action.payload.items, normalizeKey: 'id'}));
4 next(setLoader({state: false, feature: BOOKS}));
5 break;
6 ...
actions/notification.js
1 // action types
2 export const SET_NOTIFICATION = 'SET_NOTIFICATION';
3 export const REMOVE_NOTIFICATION = 'REMOVE_NOTIFICATION';
4
5 // action creators
6 export const setNotification = ({message, feature}) => ({
7 type: `${feature} ${SET_NOTIFICATION}`,
8 payload: message,
9 meta: {feature}
10 });
Action Transforming Patterns 27
11
12 export const removeNotification = ({notificationId, feature}) => ({
13 type: `${feature} ${REMOVE_NOTIFICATION}`,
14 payload: notificationId,
15 meta: {feature}
16 });
Next, we will take the notification message and enrich it with an id. Then we’ll dispatch the same
document action with the new payload. We will also dispatch an action to clear the notification
from the state:
middleware/notifications.js
• The normalize middleware transforms an action payload by converting one data structure to
another.
• The notification middleware enriches an action payload with extra information (an id).
We also learned that if a middleware performs a transformation on a document action, it won’t call
next on the first line to prevent logging duplication.
Utilities Middleware
Utilities middleware are core middleware that do not route or transform the action. In this chapter
I will introduce two common use cases for this kind of middleware:
20 next(action);
21 }
22 };
The logger middleware should be the last middleware in the core middleware chain:
store.js
1 ...
2 // create the core middleware array
3 const coreMiddleware = [
4 apiMiddleware,
5 normalizeMiddleware,
6 notificationMiddleware,
7 loggerMiddleware
8 ];
9 ...
The action splitter middleware should be the first middleware in the core middleware chain:
Utilities Middleware 32
store.js
1 // create the core middleware array
2 const coreMiddleware = [
3 actionSplitterMiddleware,
4 apiMiddleware,
5 normalizeMiddleware,
6 notificationMiddleware,
7 loggerMiddleware
8 ];
Definition
A reducer enhancer is a higher-order function that accepts a reducer as an argument and returns a
new reducer.
There are two common use cases for reducer enhancers:
The following example shows the basic structure of a reducer enhancer (that does nothing):
A reducer enhancer
reducers/undoable.js
1 export function undoable(reducer) {
2
3 // create an "upgraded" initial state
4 const initialState = {
5 past: [],
6 present: reducer(undefined, {type: '@@INIT_UNDOABLE'}),
7 };
8
9 // return a reducer that handles the new state structure
10 return function undoReducer(state = initialState, action) {
11 const {past, present} = state;
12
13 switch (action.type) {
14 case 'UNDO':
15 const previous = past[past.length - 1];
16 const newPast = past.slice(0, past.length - 1);
17 return {
18 past: newPast,
19 present: previous,
20 };
21
22 default:
23 const newPresent = reducer(present, action);
24 if (present === newPresent) {
25 return state
26 }
27 return {
28 past: [...past, present],
29 present: newPresent,
30 }
31 }
32 }
33 }
In order to enable “undo” support, we introduce a new array to the state so we can capture previous
states. We’re also returning a new reducer that knows how to handle this new state structure. If we
want to support undo functionality just for a piece of our state, we can use this reducer enhancer
on one or more individual reducers, as required.
We can also wrap our entire reducer composition (the root reducer) to support undo behavior for
the entire state.
The following example shows the use on a single reducer:
Reducer Enhancers 35
Note that I’m using a custom deepFreeze function in this code. You can use your own implementa-
tion or a helper library to achieve the same results.
Now we can wrap our root reducer with the stateFreezer to protect the entire state tree from
mutation:
Wrapping the root reducer
• Feature selectors return data (with or without manipulation) from the same node in the state
tree.
• Query selectors calculate derived data from different properties of the same node, or from other
nodes in the state tree.
In this chapter we will explore some common selection techniques using both feature and query
selectors.
Feature Selectors
Creating feature selectors is straightforward. Like most Redux components, a feature selector is a
pure function. A common practice is to create a set of selectors that return the top-level properties
of the node. We’ll explore how to do that now, and in the next section we will use those selectors to
create more complex query selectors.
Consider the following books node:
The books node
1 const state = {
2 books: {
3 selected: 3,
4 count: 4,
5 loading: false,
6 collection: {
7 1: {id: 1, title: 'book1', ...},
8 2: {id: 2, title: 'book2', ...},
9 3: {id: 3, title: 'book3', ...},
10 4: {id: 4, title: 'book4', ...}
11 }
12 }
13 }
Let’s start by creating a set of simple feature selectors for this node:
Selectors 38
We can create more focused selectors as well. For example, the following selector will return the
selected book object:
Selected book selector
In this example, we have a books map in the state. Saving the books collection as a map makes
operations like removing or updating a book very efficient. But in most cases, when we want to
render a collection of items to the view, working with arrays is much easier than working with
maps. Our next selector will convert the books map into a books array:
Structure transform selector
In some cases, we might want to create a smaller data set from the original collection. We can do
that as follows:
Selectors 39
Filtering selector
Query Selectors
Let’s add some more data to our state. For the next example, we want to retrieve an object that
contains the selected user’s profile and all of that user’s books. We need to grab the selected user’s
ID and use this to locate his library card, which contains his books’ IDs. With these IDs we can grab
the books’ details from the books collection.
Let’s start by examining the state structure:
Extended state
1 const state = {
2 books: {
3 selected: 3,
4 count: 4,
5 loading: false,
6 collection: {
7 1: {id: 1, title: 'book1'},
8 2: {id: 2, title: 'book2'},
9 3: {id: 3, title: 'book3'},
10 4: {id: 4, title: 'book4'}
11 }
12 },
13 users: {
14 selectedUser: 2,
15 list: {
16 1: {id: 1, name: 'nir', email: 'nir@500tech.com'},
17 2: {id: 2, name: 'max', email: 'max@500tech.com'},
18 3: {id: 3, name: 'jor', email:'joe@500tech.com'}
19 }
Selectors 40
20 },
21 library: {
22 cards: {
23 2: {
24 books: [3,4]
25 }
26 }
27 }
28 }
We will create a query selector by using the feature selectors from each of the features. Then we
will construct the final data structure and return it:
Query selector
Memoized Selectors
A memoized selector caches its result and returns this cached result every time the selector is invoked
with the same arguments. This performance optimization technique is very efficient for selectors
that perform heavy calculations or traverse a large data set.
I encourage you to use the official selector library for Redux, reselect, which implements memoized
selectors while exposing a clean API for creating and composing selectors.
Selectors 41
Selectors Summary
You can think of a selector as a read-only query to a database. Because selectors can’t change the
state, they won’t damage your single source of truth.
It is a good practice to create as many selectors as needed to produce optimized data structures for
view rendering, and keep the UI components stateless and clean. If you are doing heavy calculations
or transforming large data structures, use memoized selectors for performance.
Naming Conventions & Project
Structure
A typical Redux application contains a lot of components. The following list is long even without
including the rendering layer (UI components):
In this final chapter we’ll discuss some useful organizational conventions. The focus here is on
the Redux folder structure. Feel free to come up with your own conventions for the rest of your
application’s file types (constants, UI components, services, etc.).
Folder Structure
Let’s start with the overall folder structure. Since there is a clear separation between our view layer
(UI components) and state logic (Redux components), it makes sense to keep all the Redux files in a
dedicated folder which contains subfolders by type:
1 +-- src
2 | +-- components
3 | +-- redux
4 | +-- actions
5 | +-- middleware
6 | +-- core
7 | +-- feature
8 | +-- reducers
9 | +-- reducerEnhancers
10 | +-- selectors
11 | +-- store.js
Naming Conventions & Project Structure 43
This clear separation between Redux and the rest of the application parts is highly effective in the
event that you want to reuse the Redux logic in other contexts (for example, creating different
components for the same state).
Since you are going to use similar names for actions, middleware, and reducers, it’s highly important
to keep everything in the correct subfolders. We will treat the folders as namespaces when used in
an import statement:
Adopting these folder naming conventions will help you to search and identify the content types of
your files efficiently. Now, let’s take a closer look at what the subfolders should contain.
Actions
Your application will contain a lot of different types of actions. Splitting them into different files
will make those files easier to read and digest. Keeping the action type constants close to the action
creators (in the same file) will help you achieve a good separation of concerns—if you need to refactor
or change a specific set of action types, you won’t touch another file that might contain other action
type constants as well.
Following these principles, each action file will contain two distinct parts:
1 // action types
2 export const API_REQUEST = 'API_REQUEST';
3 export const API_SUCCESS = 'API_SUCCESS';
4 export const API_ERROR = 'API_ERROR';
5
6 // action creators
7 export const apiRequest = ({body, method, url, feature}) => ({
8 type: `${feature} ${API_REQUEST}`,
9 payload: body,
10 meta: {method, url, feature}
11 });
Naming Conventions & Project Structure 44
12
13 export const apiSuccess = ({response, feature}) => ({
14 type: `${feature} ${API_SUCCESS}`,
15 payload: response,
16 meta: {feature}
17 });
18
19 export const apiError = ({error, feature}) => ({
20 type: `${feature} ${API_ERROR}`,
21 payload: error,
22 meta: {feature}
23 });
Middleware
I keep both core and feature middleware in the same folder, named middleware, within subfolders
named for each type. Again, the folder name will be used as a namespace to identify the files’
contents:
1 +-- src
2 | +-- redux
3 | +-- middleware
4 | +-- core
5 | +-- normalize.js
6 | +-- api.js
7 | +-- logger.js
8 | +-- notification.js
9 | +-- feature
10 | +-- books.js
11 | +-- users.js
12 | +-- products.js
13 | +-- store.js
Reducers
I like to keep a clear separation between reducers and reducer enhancers. Reducer enhancers can
be reused in other contexts and even in other applications, and keeping them in a dedicated folder
makes it easier to move and package them:
Naming Conventions & Project Structure 45
1 +-- src
2 | +-- redux
3 | +-- reducers
4 | +-- books.js
5 | +-- users.js
6 | +-- products.js
7 | +-- ui.js
8 | +-- reducerEnhancers
9 | +-- freezer.js
10 | +-- undoable.js
11 | +-- store.js
I’ve found that keeping the initial state with both the reducer and the related feature selectors in
the same file makes the code easier to read and maintain. It also adheres to the single responsibility
principle, because both the reducer and the feature selectors are coupled to the same node in the
state tree.
With this in mind, let’s take a look at the books reducer file as an example:
reducer/books.js
1 // state structure
2 const booksState = {
3 selectedBookId: null,
4 collection: {},
5 loading: false
6 };
7
8 // compute new state (write)
9 export const booksReducer = (books = booksState, action) => {
10 switch (action.type) {
11
12 case SET_BOOKS:
13 ...
14 case UPDATE_BOOK:
15 ...
16 case REMOVE_BOOK:
17 ...
Naming Conventions & Project Structure 46
18 case SELECT_BOOK:
19 ...
20
21 default:
22 return books;
23 }
24 };
25
26 // select from state (read)
27 export const getBooksIds = state => Object.keys(state.books.collection);
28 export const getSelectedBook = state => state.books.selected[state.books.selectedBoo\
29 kId];
Reducer Enhancers
There is no special structure or convention for a reducer enhancer file. Simply exporting the function
will be enough. The only difference is that a reducer enhancer doesn’t include any selectors, and in
most cases it doesn’t include any initial state.
As a reminder, this is the stateFreezer reducer enhancer from the previous chapter:
stateFreezer.js
Query Selectors
Unlike feature selectors, query selectors that read from various nodes should be kept in a separate
selectors folder:
1 +-- src
2 | +-- redux
3 | +-- selectors
4 | +-- library.js
5 | +-- reservation.js
The Store
The store.js file is the glue that connects everything together. It resides in the top-level redux folder
and is responsible for:
1. Structuring the final shape of the state by combining reducers (using reducer enhancers, if they
exist)
2. Defining the order of core and feature middleware
3. Creating and configuring the store
The store.js file is the only place in your application where you can see the entire structure. It will
become your single source of truth. This is where you will come if you want to restructure the state,
add/remove/reorder middleware, apply reducer enhancers, or reconfigure the store creation:
store.js
15
16 import {undoable} from "./reducerEnhancers/undoable";
17 import {stateFreezer} from "./reducerEnhancers/stateFreezer";
18
19 // shape the state structure
20 const rootReducer = combineReducers({
21 books: undoable(booksReducer),
22 ui: uiReducer,
23 notification: notificationsReducer
24 });
25
26 // create the feature middleware array
27 const featureMiddleware = [
28 booksMiddleware
29 ];
30
31 // create the core middleware array
32 const coreMiddleware = [
33 actionSplitterMiddleware,
34 apiMiddleware,
35 normalizeMiddleware,
36 notificationMiddleware,
37 loggerMiddleware
38 ];
39
40 // compose the middleware with additional (optional) enhancers,
41 // DevTools.instrument() will enable dev tools integration
42 const enhancer = compose(
43 applyMiddleware(...featureMiddleware, ...coreMiddleware),
44 DevTools.instrument()
45 );
46
47 // create and configure the store
48 export const store = createStore(stateFreezer(rootReducer), {}, enhancer);
My Default Stack
The following libraries are my defaults on every Redux project. They each solve a specific problem
while using solid patterns and concepts:
Ramda
Ramda³ is a library designed specifically for a functional programming style. It comes in very handy
in Redux reducers where you need to perform data manipulations in an immutable manner. While
there are a lot of functional programming libraries out there, I like Ramda because it is very focused
(it doesn’t try to be an “all in one” solution).
reselect
As I mentioned in the chapter on state selectors, reselect⁴ is the official selectors library for Redux.
It enables you to compute derived data using memoized functions.
³https://ramdajs.com/
⁴https://github.com/reduxjs/reselect
Resources and Next Steps 50
Personally, I don’t use these libraries in my applications, simply because I’ve found it much more
straightforward to implement action processing in middleware with minimal vanilla JavaScript and
the basic messaging design patterns discussed in this book
Recommended Reading
I highly encourage you to learn more about event-driven and messaging design patterns, which will
help you adopt a new programming model and state of mind.
The following two resources are on my “must-read” list:
In general, look for CQRS and event sourcing articles across the web.
⁵https://redux-observable.js.org/
⁶https://github.com/reduxjs/redux-thunk
⁷https://redux-saga.js.org/
⁸http://www.enterpriseintegrationpatterns.com/
⁹https://docs.microsoft.com/en-us/azure/architecture/patterns/cqrs
¹⁰https://github.com/thinking-in-redux