Thinking in Redux

1,689 171 1MB

English Pages 56 [54] Year 2019

Report DMCA / Copyright

DOWNLOAD FILE

Polecaj historie

Thinking in Redux

Table of contents :
Table of Contents
About This Book
Thinking in Redux
Construct, Dispatch, Process
Action in, Action out
Getting Started
Programming with Actions
Action Intentions
The Importance of Action Type Names
Action Payload and Metadata
Action Construction
Thinking in Actions
Actions Summary
Action Routing Patterns
Core and Feature Middleware
Feature Middleware as Action Routers
The Books Feature Middleware
The API Core Middleware
Putting It All Together
Routing Patterns Summary
Action Transforming Patterns
The Normalize Core Middleware
The Notification Core Middleware
Transforming Patterns Summary
Utilities Middleware
The Logger Middleware
The Action Splitter Middleware
Utilities Middleware Summary
Reducer Enhancers
Definition
The Undoable Reducer Enhancer
Implementing a State Freezer
Reducer Enhancers Summary
Selectors
Feature Selectors
Query Selectors
Memoized Selectors
Selectors Summary
Naming Conventions & Project Structure
Folder Structure
Actions
Middleware
Reducers
Reducer Enhancers
Query Selectors
The Store
Naming Conventions Summary
Resources and Next Steps
Libraries for Cleaner Code
Alternative Middleware Implementations
Recommended Reading
Book Examples and More

Citation preview

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 . . . . . . . . Construct, Dispatch, Process Action in, Action out . . . . Getting Started . . . . . . . .

2 2 3 3

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

Programming with Actions . . . . . . . . . Action Intentions . . . . . . . . . . . . . . The Importance of Action Type Names Action Payload and Metadata . . . . . . Action Construction . . . . . . . . . . . . Thinking in Actions . . . . . . . . . . . . Actions Summary . . . . . . . . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. 5 . 5 . 8 . 9 . 9 . 10 . 16

Action Routing Patterns . . . . . . . . . . Core and Feature Middleware . . . . . Feature Middleware as Action Routers The Books Feature Middleware . . . . The API Core Middleware . . . . . . . Putting It All Together . . . . . . . . . . Routing Patterns Summary . . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

17 17 18 18 21 22 23

Action Transforming Patterns . . . . . The Normalize Core Middleware . The Notification Core Middleware Transforming Patterns Summary .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

24 24 26 29

Utilities Middleware . . . . . . . . . The Logger Middleware . . . . . The Action Splitter Middleware Utilities Middleware Summary

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

30 30 31 32

. . . .

. . . .

. . . .

. . . .

. . . .

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

CONTENTS

The Undoable Reducer Enhancer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 Implementing a State Freezer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35 Reducer Enhancers Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 Selectors . . . . . . . . . . Feature Selectors . . Query Selectors . . . Memoized Selectors Selectors Summary .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

37 37 39 40 41

Naming Conventions & Project Structure Folder Structure . . . . . . . . . . . . . . . Actions . . . . . . . . . . . . . . . . . . . . Middleware . . . . . . . . . . . . . . . . . Reducers . . . . . . . . . . . . . . . . . . . Reducer Enhancers . . . . . . . . . . . . . Query Selectors . . . . . . . . . . . . . . . The Store . . . . . . . . . . . . . . . . . . . Naming Conventions Summary . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

42 42 43 44 44 46 47 47 48

Resources and Next Steps . . . . . . . . . . . . Libraries for Cleaner Code . . . . . . . . . Alternative Middleware Implementations Recommended Reading . . . . . . . . . . . Book Examples and More . . . . . . . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

49 49 50 50 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/thinkingin-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 singlepage 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

3

Thinking in Redux

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 implementation 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 2 3 4 5 6 7 8 9

const FETCH_BOOKS = { type: 'FETCH_BOOKS', payload: { query: 'redux', }, meta: { timeout: 3000 } }

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 2 3

const API_REQUEST_PENDING = { type: 'API_REQUEST_PENDING', }

4 5 6 7 8 9 10 11 12 13 14

const ROUTING_STARTED = { type: 'ROUTING_STARTED', payload: { userId: '123456' }, meta: { from: '/home', to: '/dashboard' } }

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

7

Programming with Actions

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 2 3 4

const SET_BOOKS = { type: 'SET_BOOKS', payload: [{...},{...}] }

5 6 7 8 9

const UPDATE_BOOKS = { type: 'UPDATE_BOOKS', payload: [{...},{...}] }

10 11 12 13 14

const REMOVE_BOOK = { type: 'REMOVE_BOOK', payload: 2545852 }

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 2 3

const API_SUCCESS = { type: 'API_SUCCESS', }

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 2 3 4 5

> > > > >

'API_SUCCESS' 'API_SUCCESS' 'API_SUCCESS' 'API_SUCCESS' '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 2 3 4 5 6

> > > > > >

'[Books] '[Users] '[Books] '[Users] '[Books] '[Users]

API_SUCCESS' API_SUCCESS' API_SUCCESS' API_SUCCESS' API_SUCCESS' 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 2 3 4 5 6 7 8 9 10

const API_REQUEST = { type: 'API_REQUEST', payload: 'redux' meta: { method: 'GET', url: '/books', timeout: 3000, feature: 'Books' } }

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 2 3 4 5 6 7

function apiRequest({ params, method, url, timeout, feature }) { return { type: `${feature} API_REQUEST`, payload: params, meta: { method, url, timeout, feature } } }

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. 2. 3. 4.

Fetch a list of books from a remote server. Show a loader until you get a response. If the call was successful, hide the loader and put the books collection in the state. 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:

11

Programming with Actions

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 2

// feature name export const BOOKS = '[Books]';

3 4 5 6

// action types export const FETCH_BOOKS = `${BOOKS} FETCH`; export const SET_BOOKS = `${BOOKS} SET`;

actions/api.js 1 2 3 4

// action types export const API_REQUEST = 'API_REQUEST'; export const API_SUCCESS = 'API_SUCCESS'; export const API_ERROR = 'API_ERROR';

actions/ui.js 1 2

// action types export const SET_LOADER = 'SET_LOADER';

actions/notification.js 1 2

// action types 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 2

// feature name export const BOOKS = '[Books]';

3 4 5 6

// action types export const FETCH_BOOKS = `${BOOKS} FETCH`; export const SET_BOOKS = `${BOOKS} SET`;

7 8 9 10 11 12

action creators export const setBooks = ({books}) => ({ type: SET_BOOKS, payload: books });

Creating a command action for fetching books is straightforward as well: actions/books.js 1 2 3 4

export const fetchBooks = ({query}) => ({ type: FETCH_BOOKS, payload: query });

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 2 3 4

// action types export const API_REQUEST = 'API_REQUEST'; export const API_SUCCESS = 'API_SUCCESS'; export const API_ERROR = 'API_ERROR';

5 6 7 8 9 10 11

// action creators export const apiRequest = ({body, method, url, feature}) => ({ type: `${feature} ${API_REQUEST}`, payload: body, meta: {method, url, feature} });

12 13 14

export const apiSuccess = ({response, feature}) => ({ type: `${feature} ${API_SUCCESS}`,

Programming with Actions 15 16 17

14

payload: response, meta: {feature} });

18 19 20 21 22 23

export const apiError = ({error, feature}) => ({ type: `${feature} ${API_ERROR}`, payload: error, meta: {feature} });

actions/ui.js 1 2

// action types export const SET_LOADER = 'SET_LOADER';

3 4 5 6 7 8 9

// action creators export const setLoader = ({state, feature}) => ({ type: `${feature} ${SET_LOADER}`, payload: state, meta: {feature} });

actions/notification.js 1 2

// action types export const SET_NOTIFICATION = 'SET_NOTIFICATION';

3 4 5 6 7 8 9

// action creators export const setNotification = ({message, feature}) => ({ type: `${feature} ${SET_NOTIFICATION}`, payload: message, meta: {feature} });

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

reducers/books.js 1

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

2 3

const initState = [];

4 5 6

export const booksReducer = (books = initState, action) => { switch (action.type) {

7

case SET_BOOKS: return action.payload;

8 9 10

default: return books;

11 12 13 14

} };

reducers/ui.js 1

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

2 3 4 5

const initState = { loading: false };

6 7 8

export const uiReducer = (ui = initState, action) => { switch (true) {

9

case action.type.includes(SET_LOADER): return {...ui, loading: action.payload};

10 11 12

default: return ui;

13 14 15 16

} };

15

Programming with Actions

16

reducers/notification.js 1

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

2 3

const initState = [];

4 5 6

export const notificationsReducer = (notifications = initState, action) => { switch (true) {

7

case action.type.includes(SET_NOTIFICATION): return [...notifications, action.payload];

8 9 10

default: return notifications;

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

18

Action Routing Patterns

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 4

export const booksMiddleware = () => (next) => (action) => { next(action);

5 6

switch (action.type) {

7

case FETCH_BOOKS: // do something break;

8 9 10 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 2 3

import { FETCH_BOOKS } from "../actions/books"; import { apiRequest } from "../actions/api"; import { setLoader } from "../actions/ui";

4 5

const BOOKS_URL = 'https://www.googleapis.com/books/v1/volumes?q=redux';

6 7 8

export const booksMiddleware = () => (next) => (action) => { next(action);

9 10

switch (action.type) {

11

case FETCH_BOOKS: next(apiRequest({body: null, method: 'GET', url: BOOKS_URL, feature: BOOKS})); next(setLoader({state: true, feature: BOOKS})); break;

12 13 14 15 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 2 3 4

import import import import

{BOOKS, FETCH_BOOKS, setBooks} from "../actions/books"; {API_ERROR, API_SUCCESS, apiRequest} from "../actions/api"; {setLoader} from "../actions/ui"; {setNotification} from "../actions/notification";

5 6

const BOOKS_URL = 'https://www.googleapis.com/books/v1/volumes?q=redux';

7 8 9

export const booksMiddleware = () => (next) => (action) => { next(action);

10 11

switch (action.type) {

12

case FETCH_BOOKS: next(apiRequest({body: null, method: 'GET', url: BOOKS_URL, feature: BOOKS})); next(setLoader({state: true, feature: BOOKS})); break;

13 14 15 16 17

case `${BOOKS} ${API_SUCCESS}`: next(setBooks({books: action.payload.items})); next(setLoader({state: false, feature: BOOKS})); break;

18 19 20 21 22

case `${BOOKS} ${API_ERROR}`: next(setNotification({message: action.payload.message, feature: BOOKS})); next(setLoader({state: false, feature: BOOKS})); break;

23 24 25 26 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 4

export const apiMiddleware = ({dispatch}) => (next) => (action) => { next(action);

5 6 7 8 9

if (action.type.includes(API_REQUEST)) { // do something } };

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 4

export const apiMiddleware = ({ dispatch }) => next => action => { next(action);

5 6 7

if(action.type.includes(API_REQUEST)) { const { url, method, feature } = action.meta ;

8

fetch(url, { method }) .then( response => response.json()) .then( data => dispatch(apiSuccess(data, feature))) .catch( error => dispatch(apiError(error, feature)))

9 10 11 12 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. 2. 3. 4.

The API middleware is generic and reusable. The API middleware has only one responsibility. The API middleware is configured by command action metadata. 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 2 3 4 5 6 7

import import import import import import import

{DevTools} from '../ui/DevTool' {createStore, combineReducers, applyMiddleware, compose} from 'redux'; { booksReducer } from './reducers/books.reducer'; { booksMiddleware } from './middleware/books'; { apiMiddleware } from './middleware/api'; {uiReducer} from "./reducers/ui.reducer"; {notificationsReducer} from "./reducers/notification.reducer";

8 9 10 11 12 13 14

// shape the state structure const rootReducer = combineReducers({ books: booksReducer, ui: uiReducer, notification: notificationsReducer });

15 16 17 18 19

// create the feature middleware array const featureMiddleware = [ booksMiddleware ];

20 21 22 23 24

// create the core middleware array const coreMiddleware = [ apiMiddleware ];

25 26 27 28 29 30 31

// compose the middleware with additional (optional) enhancers, // DevTools.instrument() will enable dev tools integration const enhancer = compose( applyMiddleware(...featureMiddleware, ...coreMiddleware), DevTools.instrument() );

32 33 34

// create and configure the store 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 2 3 4 5

export const setBooks = ({books, normalizeKey}) => ({ type: SET_BOOKS, payload: books, meta: {normalizeKey, feature: BOOKS} });

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 4 5 6

export const dataNormalized = ({feature}) => ({ type: `${feature} ${DATA_NORMALIZED}`, meta: {feature} });

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 4 5 6 7 8 9

// filter both by action type and metadata content if (action.type.includes('SET') && action.meta.normalizeKey) { // normalize the raw data } else { next(action); } };

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 2

import {dataNormalized} from "../actions/data"; import {setBooks} from "../actions/books";

3 4

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

5 6 7

// filter both by action type and metadata content if (action.type.includes('SET') && action.meta.normalizeKey) {

8 9 10

// notify about the transformation dispatch(dataNormalized({feature: action.meta.feature}));

11 12 13 14 15 16

// transform the data structure const books = action.payload.reduce((acc, item) => { acc[item[action.meta.normalizeKey]] = item; return acc; }, {});

26

Action Transforming Patterns 17 18 19

// fire the books document action next({...action, payload: books, normalizeKey: null

});

20 21 22 23 24

} else { next(action); } };

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 3 4 5 6

... case `${BOOKS} ${API_SUCCESS}`: next(setBooks({books: action.payload.items, normalizeKey: 'id'})); next(setLoader({state: false, feature: BOOKS})); break; ...

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 2 3

// action types export const SET_NOTIFICATION = 'SET_NOTIFICATION'; export const REMOVE_NOTIFICATION = 'REMOVE_NOTIFICATION';

4 5 6 7 8 9 10

// action creators export const setNotification = ({message, feature}) => ({ type: `${feature} ${SET_NOTIFICATION}`, payload: message, meta: {feature} });

Action Transforming Patterns

27

11 12 13 14 15 16

export const removeNotification = ({notificationId, feature}) => ({ type: `${feature} ${REMOVE_NOTIFICATION}`, payload: notificationId, meta: {feature} });

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 4 5 6 7 8

if (action.type.includes(SET_NOTIFICATION)) { // do something } else { next(action) } };

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 2

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

3 4

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

5 6 7 8

if (action.type.includes(SET_NOTIFICATION)) { const {payload, meta} = action; const id = new Date().getMilliseconds();

9 10 11 12 13 14 15

// enrich the original payload with an id const notification = { id, massage: payload };

Action Transforming Patterns

28

// fire a new action with the enriched payload // note: the payload is an object next(setNotification({message: notification, feature: meta.feature}));

16 17 18 19

// dispatch a clear action after a given time setTimeout(() => { next(removeNotification({notificationId: id, feature: meta.feature})) }, 1000)

20 21 22 23 24 25 26 27 28

} else { next(action) } };

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 6

export const notificationsReducer = (notifications = initState, action) => { switch (true) {

7

case action.type.includes(SET_NOTIFICATION): return [...notifications, action.payload];

8 9 10

case action.type.includes(REMOVE_NOTIFICATION): return notifications.filter(notification => notification.id !== action.payload\

11 12 13

);

14

default: return notifications;

15 16 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 2

export const loggerMiddleware = ({getState}) => (next) => (action) => { const {REACT_APP_ENV} = process.env;

3 4

if (REACT_APP_ENV === 'development') {

5 6

console.group(`${action.type}`);

7 8 9 10

console.group('CURRENT STATE:'); console.log(getState()); console.groupEnd();

11 12

next(action);

13 14 15 16

console.group('NEXT STATE: '); console.log(getState()); console.groupEnd();

17 18 19

console.groupEnd(); } else {

Utilities Middleware

next(action);

20 21 22

31

} };

The logger middleware should be the last middleware in the core middleware chain: store.js 1 2 3 4 5 6 7 8 9

... // create the core middleware array const coreMiddleware = [ apiMiddleware, normalizeMiddleware, notificationMiddleware, loggerMiddleware ]; ...

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 2 3 4 5 6 7

export const actionSplitterMiddleware = () => (next) => (action) => { if (Array.isArray(action)) { action.forEach(_action => next(_action)) } else { next(action); } };

The action splitter middleware should be the first middleware in the core middleware chain:

Utilities Middleware

32

store.js 1 2 3 4 5 6 7 8

// create the core middleware array const coreMiddleware = [ actionSplitterMiddleware, apiMiddleware, normalizeMiddleware, notificationMiddleware, loggerMiddleware ];

We can take advantage of this in the books middleware: books.middleware.js 1

... case FETCH_BOOKS: next([ apiRequest({body: null, method: 'GET', url: BOOKS_URL, feature: BOOKS}), setLoader({state: true, feature: BOOKS}) ]); break;

2 3 4 5 6 7 8

case `${BOOKS} ${API_SUCCESS}`: next([ setBooks({books: action.payload.items, normalizeKey: 'id'}), setLoader({state: false, feature: BOOKS}) ]); break;

9 10 11 12 13 14 15

case `${BOOKS} ${API_ERROR}`: next([ setNotification({message: action.payload.message, feature: BOOKS}), setLoader({state: false, feature: BOOKS}) ]); break;

16 17 18 19 20 21 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 2 3 4 5

function reducerEnhancer (originalReducer) { return function newReducer(state, action) { return originalReducer(state, action); } }

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

// create an "upgraded" initial state const initialState = { past: [], present: reducer(undefined, {type: '@@INIT_UNDOABLE'}), };

3 4 5 6 7 8

// return a reducer that handles the new state structure return function undoReducer(state = initialState, action) { const {past, present} = state;

9 10 11 12

switch (action.type) { case 'UNDO': const previous = past[past.length - 1]; const newPast = past.slice(0, past.length - 1); return { past: newPast, present: previous, };

13 14 15 16 17 18 19 20 21

default: const newPresent = reducer(present, action); if (present === newPresent) { return state } return { past: [...past, present], present: newPresent, }

22 23 24 25 26 27 28 29 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 2 3 4 5 6

// shape the state structure const rootReducer = combineReducers({ books: undoable(booksReducer), ui: uiReducer, notification: notificationsReducer });

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 4 5 6 7

export function stateFreezer(reducer) { return function freezer(state, action) { // freeze the state and run the original reducer deepFreeze(state); const newState = reducer(state, action);

8

// freeze and return the result state deepFreeze(newState); return newState;

9 10 11

}

12 13

}

Note that I’m using a custom deepFreeze function in this code. You can use your own implementation 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 2

// create and configure the store 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 2 3 4 5 6 7 8 9 10 11 12 13

const state = { books: { selected: 3, count: 4, loading: false, collection: { 1: {id: 1, title: 2: {id: 2, title: 3: {id: 3, title: 4: {id: 4, title: } } }

'book1', 'book2', 'book3', 'book4',

...}, ...}, ...}, ...}

Let’s start by creating a set of simple feature selectors for this node:

38

Selectors

Books feature selector 1 2 3 4

const const const const

getBooks getBookCount getBooksLoading getSelectedBookId

= = = =

state state state state

=> => => =>

state.books.collection; state.books.count; state.books.loading; 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 2 3

const getSelectedBook = state => { const books = getBooks(state); const bookId = getSelectedBookId(state)

4

return books[bookId];

5 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 2

const getBooksArray = state => { const books = getBooks(state);

3

return Object.keys(books).reduce((bookArray = [], bookId) => { bookArray.push(books[bookId]) return bookArray; }, [])

4 5 6 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 2

const getBooksArray = state => { const books = getBooks(state);

3

return Object.keys(books).reduce((bookArray = [], bookId) => { const { title, id, author, published } = books[bookId];

4 5 6

bookArray.push({ id, title, author, published }); return bookArray; }, []);

7 8 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

const state = { books: { selected: 3, count: 4, loading: false, collection: { 1: {id: 1, title: 'book1'}, 2: {id: 2, title: 'book2'}, 3: {id: 3, title: 'book3'}, 4: {id: 4, title: 'book4'} } }, users: { selectedUser: 2, list: { 1: {id: 1, name: 'nir', email: '[email protected]'}, 2: {id: 2, name: 'max', email: '[email protected]'}, 3: {id: 3, name: 'jor', email:'[email protected]'} }

Selectors

}, library: { cards: { 2: { books: [3,4] } } }

20 21 22 23 24 25 26 27 28

40

}

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 2 3 4 5

const getSelectedUserDetails = state => { const books = getBooks(state); const users = getUsers(state); const selectedUser = getSelectedUser(state); const libraryCards = getLibraryCards(state);

6 7 8 9 10 11

const booksForUser = libraryCards[selectedUser].books.reduce( (result = [], bookId\ ) => { result.push(books[bookId]); return result; }, [])

12

return { [selectedUser]: { userProfile: users[selectedUser], books: booksForUser } };

13 14 15 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. 2. 3. 4. 5. 6. 7. 8. 9.

Action type constants Action creators Feature middleware Core middleware Reducers Reducer enhancers Feature selectors Query selectors 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 2 3 4 5 6 7 8 9 10 11

+-- src | +-- components | +-- redux | +-- actions | +-- middleware | +-- core | +-- feature | +-- reducers | +-- reducerEnhancers | +-- selectors | +-- 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 2 3

import { books } from 'actions/books'; import { books } from 'middleware/feature/books'; 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 2 3 4

// action types export const API_REQUEST = 'API_REQUEST'; export const API_SUCCESS = 'API_SUCCESS'; export const API_ERROR = 'API_ERROR';

5 6 7 8 9 10 11

// action creators export const apiRequest = ({body, method, url, feature}) => ({ type: `${feature} ${API_REQUEST}`, payload: body, meta: {method, url, feature} });

Naming Conventions & Project Structure

44

12 13 14 15 16 17

export const apiSuccess = ({response, feature}) => ({ type: `${feature} ${API_SUCCESS}`, payload: response, meta: {feature} });

18 19 20 21 22 23

export const apiError = ({error, feature}) => ({ type: `${feature} ${API_ERROR}`, payload: error, meta: {feature} });

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 2 3 4 5 6 7 8 9 10 11 12 13

+-- src | +-- redux | +-- middleware | +-- core | +-- normalize.js | +-- api.js | +-- logger.js | +-- notification.js | +-- feature | +-- books.js | +-- users.js | +-- products.js | +-- 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 1 2 3 4 5 6 7 8 9 10 11

45

+-- src | +-- redux | +-- reducers | +-- books.js | +-- users.js | +-- products.js | +-- ui.js | +-- reducerEnhancers | +-- freezer.js | +-- undoable.js | +-- 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 2 3 4 5 6

// state structure const booksState = { selectedBookId: null, collection: {}, loading: false };

7 8 9 10

// compute new state (write) export const booksReducer = (books = booksState, action) => { switch (action.type) {

11 12 13 14 15 16 17

case SET_BOOKS: ... case UPDATE_BOOK: ... case REMOVE_BOOK: ...

Naming Conventions & Project Structure

46

case SELECT_BOOK: ...

18 19 20

default: return books;

21 22 23 24

} };

25 26 27 28 29

// select from state (read) export const getBooksIds = state => Object.keys(state.books.collection); export const getSelectedBook = state => state.books.selected[state.books.selectedBoo\ 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 2 3 4

export function stateFreezer (reducer) { return function stateFreezer (state, action) { deepFreeze(state); const newState = reducer(state, action);

5

deepFreeze(newState); return newState;

6 7

}

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 2 3 4 5

+-- src | +-- redux | +-- selectors | +-- library.js | +-- 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 2

import {DevTools} from '../ui/DevTool' import {applyMiddleware, combineReducers, compose, createStore} from 'redux';

3 4 5 6

import {booksReducer} from './reducers/books.reducer'; import {uiReducer} from "./reducers/ui.reducer"; import {notificationsReducer} from "./reducers/notification.reducer";

7 8

import {booksMiddleware} from './middleware/feature/books';

9 10 11 12 13 14

import import import import import

{actionSplitterMiddleware} from "./middleware/core/actionSplitter"; {apiMiddleware} from './middleware/core/api'; {normalizeMiddleware} from "./middleware/core/normalize"; {notificationMiddleware} from "./middleware/core/notification"; {loggerMiddleware} from "./middleware/core/logger";

Naming Conventions & Project Structure

48

15 16 17

import {undoable} from "./reducerEnhancers/undoable"; import {stateFreezer} from "./reducerEnhancers/stateFreezer";

18 19 20 21 22 23 24

// shape the state structure const rootReducer = combineReducers({ books: undoable(booksReducer), ui: uiReducer, notification: notificationsReducer });

25 26 27 28 29

// create the feature middleware array const featureMiddleware = [ booksMiddleware ];

30 31 32 33 34 35 36 37 38

// create the core middleware array const coreMiddleware = [ actionSplitterMiddleware, apiMiddleware, normalizeMiddleware, notificationMiddleware, loggerMiddleware ];

39 40 41 42 43 44 45

// compose the middleware with additional (optional) enhancers, // DevTools.instrument() will enable dev tools integration const enhancer = compose( applyMiddleware(...featureMiddleware, ...coreMiddleware), DevTools.instrument() );

46 47 48

// create and configure the store 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