Sunteți pe pagina 1din 54

Thinking in Redux

Nir Kaufman
This book is for sale at http://leanpub.com/thinking-in-Redux

This version was published on 2018-07-15

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.

© 2018 Nir Kaufman


Contents

About This Book . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1

Thinking in Redux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Construct, Dispatch, Process . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
Action in, Action out . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
Getting Started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3

Programming with Actions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5


Action Intentions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
The Importance of Action Type Names . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Action Payload and Metadata . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
Action Construction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
Thinking in Actions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
Actions Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16

Action Routing Patterns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17


Core and Feature Middleware . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
Feature Middleware as Action Routers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
The Books Feature Middleware . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
The API Core Middleware . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
Putting It All Together . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
Routing Patterns Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23

Action Transforming Patterns . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24


The Normalize Core Middleware . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
The Notification Core Middleware . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
Transforming Patterns Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29

Utilities Middleware . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
The Logger Middleware . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
The Action Splitter Middleware . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
Utilities Middleware Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32

Reducer Enhancers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
Definition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
CONTENTS

The Undoable Reducer Enhancer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33


Implementing a State Freezer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
Reducer Enhancers Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36

Selectors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
Feature Selectors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
Query Selectors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
Memoized Selectors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
Selectors Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41

Naming Conventions & Project Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42


Folder Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
Actions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
Middleware . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
Reducers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
Reducer Enhancers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
Query Selectors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
The Store . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
Naming Conventions Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48

Resources and Next Steps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49


Libraries for Cleaner Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
Alternative Middleware Implementations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
Recommended Reading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
Book Examples and More . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
About This Book
This book is an opinionated guide to patterns, techniques, and conventions for using Redux. While
some parts of the book describe known and accepted conventions, other parts are based on my own
experience and personal perspective.
This is not a manual for Redux. While I will provide some background, the intended audience is
experienced developers who are already familiar with Redux concepts and terminology and are
looking for another point of view.
The concepts and examples in this book are based on plain JavaScript.

How to Read This Book


The book was designed to be a short, straight to the point guide with a lot of code examples. It is
important to read the chapters in order, because the concepts introduced in later chapters build on
what came before.
In future updates, the book will not grow beyond 80 pages of content.

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.

Construct, Dispatch, Process


Redux is all about actions, which are custom events represented as simple JavaScript objects. Every
flow implemented with Redux starts by constructing and dispatching an action. This action will be
processed by one or more middleware until a final data structure is ready to be written to the state.
Then, an action that carries this data will be processed by a reducer function that produces a new
state as a result. Because the state is a read-only immutable object, a rendering layer can read the
entire state or just a slice of it using read-only state selectors. The following diagram shows all the
components that are involved in this pattern:

Data flow in redux


Thinking in Redux 3

Putting this into words:

• 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.

Action in, Action out


The previous diagram reveals an interesting fact about Redux: the majority of action processing
(including side effects) happens in the middleware. This is a very important concept that is
sometimes missed by developers who are new to Redux. An action in Redux is a plain JavaScript
object that contains nothing but static data. The rest of a Redux application is a collection of pure
and focused simple JavaScript functions:

• 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.

The following table makes the roles easy to remember:

Function Role Input Result


Action creator Return an action Arguments Action
Middleware Process an action Action Action
Reducer Return new state State, action State

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:

1. Core middleware : reusable functions, process “generic” actions such as API_REQUEST


2. Feature middleware : process feature-specific actions such as SELECT_BOOK

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

Command action example

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 }

The action’s type name indicates the status of the application.


Programming with Actions 7

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 }

A document action will always result in a state change.

Connecting the Dots


In general, we can say that a flow always begins with a command action that triggers events (and
other command actions) and ends with a document action that contains the final structure of data
to be written to the state. The following table summarizes the intentions of the different kinds of
actions:
Action type Intention Dispatched by Processed by
Command Start a procedure UI component, middleware Middleware
Event Notify about a change Middleware Middleware
Document Write data to state Middleware Reducer

This table can help you decide where you should process your actions.
Programming with Actions 8

The Importance of Action Type Names


Action type names play a crucial part in the debugging process of our systems. The type name should
be descriptive and match the sender’s intention. Generic action type names, or a lack of namespacing,
will simply make the flow harder to track and debug. Consider the following example:
Generic action type

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

A better action log

1 > '[Books] API_SUCCESS'


2 > '[Users] API_SUCCESS'
3 > '[Books] API_SUCCESS'
4 > '[Users] API_SUCCESS'
5 > '[Books] API_SUCCESS'
6 > '[Users] API_SUCCESS'

Action Payload and Metadata


As stated at the beginning of this chapter, an action is just a bundle of data that needs to be processed
to reach its final shape. Therefore, we need to include some metadata that provides configuration
information so the middleware will be able to process the action efficiently. It is a good practice to
create a convention for the action structure that includes both data and metadata. You can include
any data that is relevant for the action’s processing, as needed. The following is an example of an
API request command action with both data and metadata:
API request action with metadata

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

1 function apiRequest({ params, method, url, timeout, feature }) {


2 return {
3 type: `${feature} API_REQUEST`,
4 payload: params,
5 meta: { method, url, timeout, feature }
6 }
7 }

The most important thing to remember is that an action creator must conform to these rules:

1. It must be a pure function (predictable, with no side effects).


2. It must be decoupled from other parts of the system (unaware of any other Redux components).
3. It must return a simple JavaScript object.

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:

1. Fetch a list of books from a remote server.


2. Show a loader until you get a response.
3. If the call was successful, hide the loader and put the books collection in the state.
4. If an error occurred, hide the loader and put a notification message in the state.

Although it is a simple flow, a lot is going on. I recommend the following workflow.

Step 1: Identify Actions by Intention


The following chart describes the actions that take place in our flow:
Programming with Actions 11

Fetch books flow

It can also be described in a simple table as follows:

Command actions Event actions Document actions


FETCH_BOOKS API_SUCCESS SET_BOOKS
API_REQUEST API_ERROR SET_LOADER
SET_NOTIFICATION

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.

Step 2: Action Construction


Now that we know which kinds of actions we need to create and have defined their type names,
we need to decide what kind of information those actions need to carry in order to be processed. In
other words, we need to define the payload and metadata for the actions. Let’s start by implementing
action creators for the books actions. Since those actions simply carry data for the reducer, this is
straightforward:
Programming with Actions 13

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 });

Creating a command action for fetching books is straightforward as well:


actions/books.js

1 export const fetchBooks = ({query}) => ({


2 type: FETCH_BOOKS,
3 payload: query
4 });

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 });

Step 3: Create a Reducer for Document Actions


As we already know, reducers are pure functions that accept a document action and pass its payload
to the state. That means we can already implement all the reducers for this flow right now:
Programming with Actions 15

reducers/books.js

1 import {SET_BOOKS} from "../actions/books.actions";


2
3 const initState = [];
4
5 export const booksReducer = (books = initState, action) => {
6 switch (action.type) {
7
8 case SET_BOOKS:
9 return action.payload;
10
11 default:
12 return books;
13 }
14 };

reducers/ui.js

1 import {SET_LOADER} from "../actions/ui.actions";


2
3 const initState = {
4 loading: false
5 };
6
7 export const uiReducer = (ui = initState, action) => {
8 switch (true) {
9
10 case action.type.includes(SET_LOADER):
11 return {...ui, loading: action.payload};
12
13 default:
14 return ui;
15 }
16 };
Programming with Actions 16

reducers/notification.js

1 import {SET_NOTIFICATION} from "../actions/notification.actions";


2
3 const initState = [];
4
5 export const notificationsReducer = (notifications = initState, action) => {
6 switch (true) {
7
8 case action.type.includes(SET_NOTIFICATION):
9 return [...notifications, action.payload];
10
11 default:
12 return notifications;
13 }
14 };

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:

• Routing patterns, for mapping an action to one or more different actions


• Transforming patterns, for manipulating the action payload

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 and Feature Middleware


As I mentioned briefly in the introduction, I suggest splitting middleware into two categories. Each
one plays a different role in the system, and they have different conventions and structure.

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 as Action Routers


You can think of a feature middleware as an action router. This middleware accepts a command
action, which it processes (possibly by passing it back and forth to the core middleware). Finally, it
is the feature middleware that will dispatch the final document action for the reducer to process.
The following diagram demonstrates the relationship between the types of middleware:

Feature middleware

In the rest of this chapter we will implement both core middleware and feature middleware.

The Books Feature Middleware


The books middleware is a feature middleware that processes actions related to the Books feature. It
processes a command action and is responsible for dispatching a document action at the end of its
processing.
In between, the books middleware will dispatch a command action for an API request that will be
processed by an API middleware.

The Filter and Split Patterns


Every action processor starts by deciding which actions to process and which to ignore. Since all
actions go through all the middleware (and reducers) in a system, we can consider this as a filtering
operation.
We start by filtering only the actions that relate to the Books feature. The first action that we care
about is FETCH_BOOKS, which is a command action:
Action Routing Patterns 19

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:

1. An API_REQUEST command action that will be processed by the API middleware


2. A SET_LOADER document action that will be processed by the UI reducer

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:

1. In case of an API_SUCCESS action, dispatch SET_BOOKS and SET_LOADER.


2. In case of an API_ERROR action, dispatch SET_NOTIFICATION and SET_LOADER.

middleware/books.js

1 import {BOOKS, FETCH_BOOKS, setBooks} from "../actions/books";


2 import {API_ERROR, API_SUCCESS, apiRequest} from "../actions/api";
3 import {setLoader} from "../actions/ui";
4 import {setNotification} from "../actions/notification";
5
6 const BOOKS_URL = 'https://www.googleapis.com/books/v1/volumes?q=redux';
7
8 export const booksMiddleware = () => (next) => (action) => {
9 next(action);
10
11 switch (action.type) {
12
13 case FETCH_BOOKS:
14 next(apiRequest({body: null, method: 'GET', url: BOOKS_URL, feature: BOOKS}));
15 next(setLoader({state: true, feature: BOOKS}));
16 break;
17
18 case `${BOOKS} ${API_SUCCESS}`:
19 next(setBooks({books: action.payload.items}));
20 next(setLoader({state: false, feature: BOOKS}));
21 break;
22
23 case `${BOOKS} ${API_ERROR}`:
24 next(setNotification({message: action.payload.message, feature: BOOKS}));
25 next(setLoader({state: false, feature: BOOKS}));
26 break;
27 }
28 };

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 Books Feature Middleware—Review


Our books middleware is now ready. We used a common pattern: splitting an action into two (or
more) actions. This pattern helps us to:

1. Split the action processing between core middleware.


2. Keep our books middleware decoupled from the rest of the system.
3. Keep our reducers clean and focused.

Next, we will implement the API core middleware.

The API Core Middleware


The API middleware is a core middleware responsible for communicating with the server via HTTP
API calls. It processes an API_REQUEST command action and dispatches an API_SUCCESS or API_ERROR
event action, depending on the result of the call.
This middleware is considered a core middleware because it accepts a command action that can
be dispatched in different contexts and dispatches as a result an event action that can be processed
by other middleware. This middleware doesn’t care about the action sender, and has no knowledge
about when and how the events it dispatches will be further processed.

The Integrity Key and Map Patterns


In our case, we want to process the API_REQUEST command action. As you’ll recall from the previous
chapter, each API_REQUEST includes a prefix specific to that call. We pass this prefix to the next action
to preserve the integrity of our flow: it will help us identify related actions while debugging. This
technique is known as the integrity key pattern.
Because we want to keep our middleware generic, we will need to ignore the prefix in order to
identify the action type. This can be done as follows:
middleware/api.js

1 import {API_REQUEST} from "../actions/api";


2
3 export const apiMiddleware = ({dispatch}) => (next) => (action) => {
4 next(action);
5
6 if (action.type.includes(API_REQUEST)) {
7 // do something
8 }
9 };
Action Routing Patterns 22

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

1 import {API_REQUEST, apiSuccess, apiError} from "../actions/api.actions";


2
3 export const apiMiddleware = ({ dispatch }) => next => action => {
4 next(action);
5
6 if(action.type.includes(API_REQUEST)) {
7 const { url, method, feature } = action.meta ;
8
9 fetch(url, { method })
10 .then( response => response.json())
11 .then( data => dispatch(apiSuccess(data, feature)))
12 .catch( error => dispatch(apiError(error, feature)))
13 }
14 };

The API Middleware Review


Our API core middleware is now ready. We used a common pattern: mapping an action to a different
action while maintaining integrity using the feature name.
All of the following are also true:

1. The API middleware is generic and reusable.


2. The API middleware has only one responsibility.
3. The API middleware is configured by command action metadata.
4. The API middleware is completely decoupled from the rest of our application.

Putting It All Together


Let’s connect everything together so we can test our action flow so far. Create a new file named
store.js. This is where we will create and configure the store with the reducers and middleware:
Action Routing Patterns 23

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 );

Routing Patterns Summary


In this chapter we implemented an action flow using core middleware for handling server
communication and a feature-specific feature middleware for managing the books collection.
We used four action routing patterns: filter, map, split, and integrity key (feature name).
Action Transforming Patterns
In the previous chapter we implemented several common routing patterns. In this chapter we will
explore two common action transforming patterns: enrich and normalize. These patterns, as the
name suggests, will transform the action payload.

The Normalize Core Middleware


The normalize middleware is responsible for transforming a raw response from a server call into
an optimized data structure. In other words, this middleware will transform one data structure to
another.
This is a core middleware, which means that it shouldn’t be coupled to a specific feature in our
system. Like with the API middleware from the previous chapter, we will use the action metadata
to determine whether the payload should be optimized, and how.
To start, we need to refactor the SET_BOOKS action creator to include some missing pieces of metadata:
actions/books.js

1 export const setBooks = ({books, normalizeKey}) => ({


2 type: SET_BOOKS,
3 payload: books,
4 meta: {normalizeKey, feature: BOOKS}
5 });

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

1 const DATA_NORMALIZED = 'DATA_NORMALIZED';


2
3 export const dataNormalized = ({feature}) => ({
4 type: `${feature} ${DATA_NORMALIZED}`,
5 meta: {feature}
6 });
Action Transforming Patterns 25

The Transform Pattern


Now we are ready to implement the normalize middleware. The first step is to filter only document
actions that have a normalizeKey defined in their metadata. Considering the action payload or
metadata when filtering actions is a common technique known as content-aware filtering:
middleware/normalize.js

1 export const normalizeMiddleware = ({dispatch}) => (next) => (action) => {


2
3 // filter both by action type and metadata content
4 if (action.type.includes('SET') && action.meta.normalizeKey) {
5 // normalize the raw data
6 } else {
7 next(action);
8 }
9 };

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

1 import {dataNormalized} from "../actions/data";


2 import {setBooks} from "../actions/books";
3
4 export const normalizeMiddleware = ({dispatch}) => (next) => (action) => {
5
6 // filter both by action type and metadata content
7 if (action.type.includes('SET') && action.meta.normalizeKey) {
8
9 // notify about the transformation
10 dispatch(dataNormalized({feature: action.meta.feature}));
11
12 // transform the data structure
13 const books = action.payload.reduce((acc, item) => {
14 acc[item[action.meta.normalizeKey]] = item;
15 return acc;
16 }, {});
Action Transforming Patterns 26

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 ...

Don’t forget to fix the books reducer to reflect the changes.

The Notification Core Middleware


This middleware is responsible for enriching the SET_NOTIFICATION document action before it hits
the reducer, and it dispatches a REMOVE_NOTIFICATION document action to clean the notification from
the state after a given time.
To achieve this we will use another action transforming pattern. But first, let’s create the REMOVE_-
NOTIFICATION document action:

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 });

The Enrich Pattern


Let’s create a middleware for processing the SET_NOTIFICATION action:
middleware/notification.js

1 export const notificationMiddleware = () => (next) => (action) => {


2
3 if (action.type.includes(SET_NOTIFICATION)) {
4 // do something
5 } else {
6 next(action)
7 }
8 };

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

1 import {removeNotification, SET_NOTIFICATION, setNotification} from "../actions/noti\


2 fication";
3
4 export const notificationMiddleware = () => (next) => (action) => {
5
6 if (action.type.includes(SET_NOTIFICATION)) {
7 const {payload, meta} = action;
8 const id = new Date().getMilliseconds();
9
10 // enrich the original payload with an id
11 const notification = {
12 id,
13 massage: payload
14 };
15
Action Transforming Patterns 28

16 // fire a new action with the enriched payload


17 // note: the payload is an object
18 next(setNotification({message: notification, feature: meta.feature}));
19
20 // dispatch a clear action after a given time
21 setTimeout(() => {
22 next(removeNotification({notificationId: id, feature: meta.feature}))
23 }, 1000)
24
25 } else {
26 next(action)
27 }
28 };

Don’t forget to update the notification reducer to reflect the changes:


reducers/notifications.js

1 import {REMOVE_NOTIFICATION, SET_NOTIFICATION} from "../actions/notification";


2
3 const initState = [];
4
5 export const notificationsReducer = (notifications = initState, action) => {
6 switch (true) {
7
8 case action.type.includes(SET_NOTIFICATION):
9 return [...notifications, action.payload];
10
11 case action.type.includes(REMOVE_NOTIFICATION):
12 return notifications.filter(notification => notification.id !== action.payload\
13 );
14
15 default:
16 return notifications;
17 }
18 };
Action Transforming Patterns 29

Transforming Patterns Summary


In this chapter we introduced two common transforming patterns, transform and enrich:

• 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:

• A logger middleware for debugging and monitoring the action flow


• An action splitter middleware, which is a helper to support dispatching an array of actions

The Logger Middleware


A logger middleware is a utility function for debugging purposes. It logs actions and state snapshots.
Although a lot of developers use a logger middleware to log every action in the system, I find this
unnecessary because not all actions result in a new state. Instead, I suggest logging only document
actions.
The logger middleware is a good example of a “context-aware” middleware because in most cases,
we will want to log only in the development environment.
The following example is a suggested implementation for a logger middleware:
middleware/logger.js

1 export const loggerMiddleware = ({getState}) => (next) => (action) => {


2 const {REACT_APP_ENV} = process.env;
3
4 if (REACT_APP_ENV === 'development') {
5
6 console.group(`${action.type}`);
7
8 console.group('CURRENT STATE:');
9 console.log(getState());
10 console.groupEnd();
11
12 next(action);
13
14 console.group('NEXT STATE: ');
15 console.log(getState());
16 console.groupEnd();
17
18 console.groupEnd();
19 } else {
Utilities Middleware 31

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


As we saw earlier in the book, splitting an action into two or more actions is a common routing
pattern. The action splitter middleware is a utility function that helps us clean up our code by
providing the ability to dispatch an array of actions instead of calling next multiple times. This
is its only real purpose—in other words, it only affects code style.
The implementation is straightforward:
actionSplitter.js

1 export const actionSplitterMiddleware = () => (next) => (action) => {


2 if (Array.isArray(action)) {
3 action.forEach(_action => next(_action))
4 } else {
5 next(action);
6 }
7 };

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 ];

We can take advantage of this in the books middleware:


books.middleware.js
1 ...
2 case FETCH_BOOKS:
3 next([
4 apiRequest({body: null, method: 'GET', url: BOOKS_URL, feature: BOOKS}),
5 setLoader({state: true, feature: BOOKS})
6 ]);
7 break;
8
9 case `${BOOKS} ${API_SUCCESS}`:
10 next([
11 setBooks({books: action.payload.items, normalizeKey: 'id'}),
12 setLoader({state: false, feature: BOOKS})
13 ]);
14 break;
15
16 case `${BOOKS} ${API_ERROR}`:
17 next([
18 setNotification({message: action.payload.message, feature: BOOKS}),
19 setLoader({state: false, feature: BOOKS})
20 ]);
21 break;
22 }
23 ...

Utilities Middleware Summary


A utility middleware performs operations that don’t alter or interfere with the original actions and
data flow. The most common scenario is having an action splitter middleware at the start of the
chain and a logger at the end.
Reducer Enhancers
Up to now, we’ve been exploring different patterns for action processing and side effect management
using middleware.
In this chapter I introduce the concept of reducer enhancers, which enable us to enhance an existing
reducer without modifying its code.

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:

• Wrapping a single reducer to enhance a specific part of the state


• Wrapping the entire reducer composition (root reducer) to affect the entire state

The following example shows the basic structure of a reducer enhancer (that does nothing):
A reducer enhancer

1 function reducerEnhancer (originalReducer) {


2 return function newReducer(state, action) {
3 return originalReducer(state, action);
4 }
5 }

The Undoable Reducer Enhancer


Here, we’ll create an undoable reducer enhancer that will wrap every reducer that needs to support
an undo capability. Let’s jump straight to the code:
Reducer Enhancers 34

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

Wrapping a single reducer

1 // shape the state structure


2 const rootReducer = combineReducers({
3 books: undoable(booksReducer),
4 ui: uiReducer,
5 notification: notificationsReducer
6 });

Implementing a State Freezer


Our next example will be a utility reducer enhancer. It will “freeze” the entire state object to protect
it from mutation. Consider the following implementation:
reducers/stateFreezer.js

1 import {deepFreeze} from "../../utils/deepFreeze";


2
3 export function stateFreezer(reducer) {
4 return function freezer(state, action) {
5 // freeze the state and run the original reducer
6 deepFreeze(state);
7 const newState = reducer(state, action);
8
9 // freeze and return the result state
10 deepFreeze(newState);
11 return newState;
12 }
13 }

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

1 // create and configure the store


2 export const store = createStore(stateFreezer(rootReducer), {}, enhancer);
Reducer Enhancers 36

Reducer Enhancers Summary


By using reducer enhancers we can extend the behavior of an existing reducer without altering its
code. This concept of a function that wraps another function is known as a higher-order function.
You can find the same pattern in component architecture; these components are called higher-order
components (HOCs).
Selectors
A selector is a helper function that takes the entire state as an argument and returns part of it as a
result. We use selectors to compute derived data from the state for rendering a view. A selector can
return a single object from the state tree as-is or a new data structure created by joining multiple
objects and data from different places in the state tree.
There are two main types of selectors:

• 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

Books feature selector

1 const getBooks = state => state.books.collection;


2 const getBookCount = state => state.books.count;
3 const getBooksLoading = state => state.books.loading;
4 const getSelectedBookId = state => state.books.selected;

We can create more focused selectors as well. For example, the following selector will return the
selected book object:
Selected book selector

1 const getSelectedBook = state => {


2 const books = getBooks(state);
3 const bookId = getSelectedBookId(state)
4
5 return books[bookId];
6 }

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

1 const getBooksArray = state => {


2 const books = getBooks(state);
3
4 return Object.keys(books).reduce((bookArray = [], bookId) => {
5 bookArray.push(books[bookId])
6 return bookArray;
7 }, [])
8 }

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

1 const getBooksArray = state => {


2 const books = getBooks(state);
3
4 return Object.keys(books).reduce((bookArray = [], bookId) => {
5 const { title, id, author, published } = books[bookId];
6
7 bookArray.push({ id, title, author, published });
8 return bookArray;
9 }, []);
10 }

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

1 const getSelectedUserDetails = state => {


2 const books = getBooks(state);
3 const users = getUsers(state);
4 const selectedUser = getSelectedUser(state);
5 const libraryCards = getLibraryCards(state);
6
7 const booksForUser = libraryCards[selectedUser].books.reduce( (result = [], bookId\
8 ) => {
9 result.push(books[bookId]);
10 return result;
11 }, [])
12
13 return { [selectedUser]: {
14 userProfile: users[selectedUser],
15 books: booksForUser
16 } };
17 }

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):

1. Action type constants


2. Action creators
3. Feature middleware
4. Core middleware
5. Reducers
6. Reducer enhancers
7. Feature selectors
8. Query selectors
9. Store

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:

1 import { books } from 'actions/books';


2 import { books } from 'middleware/feature/books';
3 import { books } from 'reducers/books';

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:

• Action type constants at the top


• Action creator functions below

As a reminder, let’s revisit the api action file:


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 });
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

A reducer file is made up of three distinct parts:

1. The initial state—what the state looks like (its structure)


2. The reducer function—how to calculate a new state
3. State selectors—how to read the state

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

1 export function stateFreezer (reducer) {


2 return function stateFreezer (state, action) {
3 deepFreeze(state);
4 const newState = reducer(state, action);
5
6 deepFreeze(newState);
7 return newState;
8 }
9 }
Naming Conventions & Project Structure 47

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

1 import {DevTools} from '../ui/DevTool'


2 import {applyMiddleware, combineReducers, compose, createStore} from 'redux';
3
4 import {booksReducer} from './reducers/books.reducer';
5 import {uiReducer} from "./reducers/ui.reducer";
6 import {notificationsReducer} from "./reducers/notification.reducer";
7
8 import {booksMiddleware} from './middleware/feature/books';
9
10 import {actionSplitterMiddleware} from "./middleware/core/actionSplitter";
11 import {apiMiddleware} from './middleware/core/api';
12 import {normalizeMiddleware} from "./middleware/core/normalize";
13 import {notificationMiddleware} from "./middleware/core/notification";
14 import {loggerMiddleware} from "./middleware/core/logger";
Naming Conventions & Project Structure 48

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);

Naming Conventions Summary


Defining and enforcing conventions for naming and structure is crucial when working with Redux
at scale. I encourage you to adopt or define a set of conventions at an early stage of your project,
and stick to them.
Resources and Next Steps
This appendix provides some references to other resources and code libraries that will help you dive
deeper into event-driven programming concepts and patterns and clean up your code.

Libraries for Cleaner Code


In this book I used vanilla JavaScript in order to show you that you don’t really need any additional
libraries to implement a complicated data flow with Redux.
I encourage you to avoid using helper libraries until you feel comfortable with the concepts of Redux
and the techniques I demonstrated in this book.
Once you feel you have a solid foundation, there are some great helper libraries that you can use
for implementing the patterns and cleaning up your code.

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:

• For keeping immutability in my reducers, I use Ramda.


• For performance in my state selectors, I use reselect.

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

Alternative Middleware Implementations


When it comes to handling side effects in Redux in middleware, there are three popular libraries out
there. Each one offers a different approach to the same problem:

• redux-observable⁵ is built on top of RxJS (a library for “reactive” programming using


observables). It treats the action flow as a stream that you can subscribe to and manipulate
in a reactive programming model.
• redux-thunk⁶ is based on promises; it gives the action creator more responsibilities by enabling
the option to dispatch an action that returns a promise.
• redux-saga⁷ makes use of JavaScript generators to handle side effects in middleware.

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:

• Enterprise Integration Patterns⁸ by Gregor Hohpe and Bobby Woolf


• “Command and Query Responsibility Segregation (CQRS) Pattern”⁹

In general, look for CQRS and event sourcing articles across the web.

Book Examples and More


You can find the code examples in the book’s repository on GitHub: github.com/thinking-in-redux¹⁰.
You can also contact me directly with any questions and for pointers to further resources via the
book’s landing page (look for the “Email the Author” link). Follow me on Twitter (@nirkaufman)
and learn more about my workshops and other publications.

⁵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

S-ar putea să vă placă și