The Redux ecosystem as a whole is flourishing and extensive. Redux’s author, Dan Abramov, has put together some great documentation and the community has done an excellent job growing the Redux ecosystem. However, a lot of knowledge is still spread across many examples, tutorials, Github issues, and engineers. In this section we will cover a condensed version of useful lessons, FAQs, and tips learned from the trenches, and guide you around common questions and pitfalls when scaling a non-trivial real world Redux application. We will tackle the following:
In real world applications, simple synchronous actions are not enough to handle everything your app will do. You’ll likely need to handle async behavior, deal with promises, trigger multiple actions, or dive into more complex recipes. Although there’s no specific rule for how you should structure your actions and action creators, here are some general insights.
action
is a plain old JavaScript object (POJO). It consists of a type
field and optional data (often stored in a payload
field) that describes a change for your application.action creator
is a function that creates an action. They do not dispatch to the store (unlike Flux), yet they return action objects and may contain additional logic to prepare an action object.dispatch()
function to trigger a state change.bindActionCreators
turns an object whose values are action creators, into an object with the same keys,
but with every action creator wrapped into a dispatch call so they may be directly invoked.Redux thunk
is an extremely useful middleware that uses thunks to solve common problems you’ll likely encounter. It allows you to write action creators that return a function instead of an action. You can use thunks to delay the dispatch of an action, or to dispatch only if a certain condition is met. They are also used when creating Async Action Creators.Serializable actions are at the heart of Redux’s defining features and enable time travel and replaying actions. It is okay to use Promises or other non-serializable values in an action as long as it is intended for use by middleware. Actions need to be serializable by the time they actually reach the store and are passed to the reducers.
Action types should be defined as self-descriptive string constants helping to reduce repetitiveness, reproduce issues, and facilitate debugging.
Defining types in a constants module can help encourage you to think early on about how you want to organize your app. This can act as a manifest for all of the things that can occur in your app. This can be extremely useful when discussing the app with other devs (use the action list as a reference), or with other teams, such as discussing actions with your UX or analytics team.
Organizing action types by export
:
// constants/ActionTypes.js
export
const
ADD_NOTE
=
'ADD_NOTE'
;
export
const
DELETE_NOTE
=
'DELETE_NOTE'
;
export
const
FAVORITE_NOTE
=
'FAVORITE_NOTE'
;
Organizing action types with keyMirror
:
// constants/ActionTypes.js
import
keyMirror
from
'keymirror'
;
const
ActionTypes
=
keyMirror
({
NOTES_REQUEST
:
null
,
// yes! less typing
NOTES_SUCCESS
:
null
,
NOTES_FAILURE
:
null
});
export
default
ActionTypes
;
The advantage to organizing your action type constants with keyMirror
is to reduce the likelihood of typos. The keyMirror
module is also used in many traditional Flux examples (by Facebook), and perhaps may be familiar to you. If you are migrating a traditional Flux app, you do not have to change the way your action types are organized.
The downside is that it becomes a little more difficult to import your constants individually using ES6, since you’ll have to import the entire object that was returned by keyMirror
. Another downside is that it becomes more difficult for static analysis and IDEs to track down where constants are defined.
To organize action types along side your action creators:
// actions/ApiActions.js
// not going to forget to declare these constants in another file
export
const
NOTES_REQUEST
=
'NOTES_REQUEST'
;
export
const
NOTES_SUCCESS
=
'NOTES_SUCCESS'
;
export
const
NOTES_FAILURE
=
'NOTES_FAILURE'
;
export
function
fetchNotes
()
{
return
(
dispatch
)
=>
{
dispatch
({
type
:
types
.
NOTES_REQUEST
});
return
fetch
(
url
,
options
)
.
then
(
parseResponse
)
.
then
(
checkStatus
)
.
then
(
normalizeJSON
);
.
then
((
json
)
=>
dispatch
({
type
:
types
.
NOTES_SUCCESS
,
payload
:
json
}))
.
catch
((
error
)
=>
dispatch
({
type
:
types
.
NOTES_FAILURE
,
payload
:
error
,
error
:
true
}));
};
}
The advantage to organizing your action constants next to your action creators is to keep your code condensed with minimal duplication. This becomes useful if your app is fairly small, with only a small amount of action types. It also becomes easier to reference these constants when editing your action creators. The downside depends on the size of your application. If you have a lot of actions or complex action creators, such as middleware, it may be better to split your constants into a separate folder and/or files for easier reuse and smaller action creator file sizes. Of course, it depends on your app and your preferred file/folder structure.
Another downside to this organization is that your reducers need to access action constants defined next to your action creators. If you want to prevent duplication, they need to be exported, and your reducers would then import them directly from your action creator modules. At this point, perhaps it is easier to keep them in a seperate file.
In order to import your constants:
// actions/ApiActions.js
import
ActionTypes
from
'../constants/ActionTypes'
;
const
{
NOTES_REQUEST
,
NOTES_SUCCESS
,
NOTES_FAILURE
}
=
ActionTypes
;
// Will I end up duplicating anyways?
function
createFetchActionCreator
(
options
,
types
){
//deconstruct
const
[
NOTES_REQUEST
,
NOTES_SUCCESS
,
NOTES_FAILURE
]
=
types
;
//...
}
const
fetchNotes
=
createFetchActionCreator
(
options
,
[
NOTES_REQUEST
,
NOTES_SUCCESS
,
NOTES_FAILURE
]);
// ...
dispatch
(
fetchNotes
());
// actions/ApiActions.js
import
ActionTypes
from
'../constants/ActionTypes'
;
// is the entire object needed?
// ...
dispatch
({
type
:
ActionTypes
.
NOTES_REQUEST
});
// Does repeating the object become ugly?
// actions/ApiActions.js
import
{
NOTES_REQUEST
,
NOTES_SUCCESS
,
NOTES_FAILURE
}
from
'../constants/ActionTypes'
;
// ...
dispatch
({
type
:
NOTES_REQUEST
});
// this is specific
Keep in mind that the way you organize your constants can make it easier to import. Consider the fact that you may have to import in several places, such as within your custom middleware and your reducers. Depending on how you organize these, you may only want to import one or two action types, as opposed to an entire object, and benefit when optimizing by being specific on what action types are actually used within your module.
Naming actions is extremely important, because your actions describe what your app can do and what your action does. As your app grows, the easier it is to understand what an action does, and the easier it will be to quickly traverse your application. When naming actions (the action type constant and action creator) it can be helpful to derive the name of an action directly from a sentence that describes what you want your app to do (creating statements of functionality).
//When the page loads, we want our app to Fetch notes
//becomes: FETCH_NOTES, dispatched on page load
//When a user clicks the checkbox it will Filter the notes list
//becomes: FILTER_NOTES, dispatched when user checks the checkbox
//The radio button will Toggle the visibility of favorited notes
//becomes: TOGGLE_NOTES_VISIBILITY, dispatched when the radio button is toggled
Avoid going beyond three segments in your action name. This may be a first sign that something else in your app needs to be fixed or addressed. Make sure that it is clear on what your action does. Being too vague can be painful for other developers jumping into the project. If you are doing it right, you should not have to comment or explain your actions.
Good:
export
const
ADD_NOTE
=
'ADD_NOTE'
;
Bad:
export
const
NOTE
=
'NOTE'
;
// fine, we have notes, but am I adding, deleting, or favoriting?
Bad:
export
const
ADD
=
'ADD'
;
// add what?
Be careful not to use terms that may mean something else within your app.
Good:
export
const
RESET_ERRORS
=
'RESET_ERRORS'
;
Bad:
export
const
RESET_STATE
=
'RESET_STATE'
;
// reset the entire state tree? or just the errors?
Your actions should be specific and typically only describe doing one thing at a time. This makes it easier to understand, and follows the single purpose rule, making it easier for add/deleting and refactoring. Keep in mind that you may have actions that result in multiple things occuring. Multiple reducers can listen to the same action and the intention of the action may actually impact multiple parts of your state; however, we should still aim to name our actions in a singular manner to avoid confusion and to maintain consistent verbage.
Good:
export
const
DELETE_NOTE
=
'DELETE_NOTE'
;
export
const
SHOW_FORM
=
'SHOW_FORM'
;
Bad:
export
const
DELETE_NOTE_AND_SHOW_FORM
=
'DELETE_NOTE_AND_SHOW_FORM'
;
Good:
export
const
LOAD_PROJECT
=
'LOAD_PROJECT'
;
// handled my multiple reducers and many things occuring when dispatched
Bad:
export
const
LOAD_PROJECT_AND_LOAD_ENTITIES
=
'LOAD_PROJECT_AND_LOAD_ENTITIES'
;
// do not describe implementation details or what your reducers are expected to do in the action name. Describe the intention of the action. If it is not clear, consider introducing another action.
It is important to decide on a convention early on with your team.
You may be inclined to reuse an action or a type for multiple things and attach a dedicated status field in your actions (Bad):
{
type
:
'FETCH_POSTS'
}
//bad, not sure if this is the start or end of async flow, need to type check
{
type
:
'FETCH_POSTS'
,
status
:
'error'
,
error
:
'Oops'
}
//bad, now my reducer needs to figure out more, and this isn't a standard pattern
{
type
:
'FETCH_POSTS'
,
status
:
'success'
,
response
:
{
...
}
}
// does not follow Flux Standard Action, may need to normalize in multiple places
Although this does work for trivial applications, it can become confusing as you scale, especially during debugging. Imagine what your Redux devtools list/logs will look like with these action types. You now have to expand and examine every single action every time this action is dispatched to determine if it is the start, error, or success. It may cause more conditions and type checking within your reducers, making it likely that you will need to duplicate, especially if logic in your actions are used in multiple reducers. It also becomes more difficult to write middleware or reuse other’s middleware because it does not follow a standard pattern.
You can and should define separate types for distinct actions, even if it feels verbose. Multiple types leave less room for a mistake, make it easier to debug, and reduce ambiguity (Good):
{
type
:
'POSTS_REQUEST'
}
// nice, I can use this to toggle a loader on
{
type
:
'POSTS_FAILURE'
,
error
:
true
,
payload
:
new
Error
(
'oops...'
)
}
// i can check the error field within my error handling reducer very easily, I also have access to the error
{
type
:
'POSTS_SUCCESS'
,
payload
:
{
...
}
}
// if i only care about handling data, I know exactly what action to listen to, in my middleware I can also spot types postfixed with _SUCCESS
As your app grows it is very obvious what each type does. It is also very easy to reuse the action across multiple reducers and easily understand them as context may be different in different reducers. For instance, POSTS_SUCCESS in your posts.js reducer may be used to update your list of posts in the post list component. It may also be used in the navigation reducer and navigation component to update the count pill on the navigation. Finally, remember that others will likely be working on the project, so defining your naming conventions early on can facilitate when devs copy and paste, or need to quickly ramp up on a new section of code.
Examining the types:
{
type
:
'POSTS_REQUEST'
}
An action informs reducers that the request began. Reducers can use this to toggle on a loader (isFetching
flag in your state).
{
type
:
'POSTS_FAILURE'
,
error
:
true
,
payload
:
new
Error
(
'oops...'
)
}
An action informing reducers that the request failed. Reducers can use this to reset isFetching
state or display error messages.
{
type
:
'POSTS_SUCCESS'
,
payload
:
{
...
}
}
An action informs the reducers that the request finished successfully.
Although it is not required, as actions only require a type, it is recommended to use the Flux Standard Action format for your actions (also called FSA’s). FSA’s define simple guidelines for a common format. See the official documentation: https://github.com/acdlite/flux-standard-action
It is much easier to debug and work with actions if we use a common format. Using a common format can facilitate sharing code or extracting logic into middlewares. We can also use the flux-standard-action module to ensure our actions are compliant.
In a real world app we can create FSA compliant actions by convention, and use FSA’s isFSA
helper function in our tests for verification. FSA’s are extremely useful when creating middleware. Because we are using a standard, we can create middleware that depends on particular action properties and write reliable and efficient code to handle these properties. A common example is intercepting meta data attached to our actions within Analytics Middleware mentioned in previous chapters.
If you need to share data between two reducers, the fastest (typical way) is to use Redux thunk and access state through getState()
. An action creator can retrieve additional data and put it in an action so that each reducer has what it needs to update its own slice of state. Notice that the inner function receives the store method dispatch and getState
as its parameters.
function
addNote
(){
return
(
dispatch
,
getState
)
=>
{
// i can get needed state for my action to make sense without passing more props
// i have access to the entire state, if i wanted, i could reuse a selector here
const
{
name
}
=
getState
();
dispatch
({
type
:
ADD_NOTE
,
payload
:
{
note
:
'my note'
,
author
:
name
}
});
};
}
store
.
dispatch
(
addNote
());
General insights:
Generally, reducers contain the business logic for determining the next state. However, reducers only execute after actions are dispatched. Thunks allow you to read the current state of the store and subsequently perform conditional dispatches before your actions reach your reducers. It is best to use this pattern if you are not able to perform the logic within your reducers, or you would like to extract out complex logic from your view.
function
incrementIfOdd
()
{
return
(
dispatch
,
getState
)
=>
{
const
{
counter
}
=
getState
();
if
(
counter
%
2
===
0
)
{
return
;
}
dispatch
(
increment
());
};
}
store
.
dispatch
(
incrementIfOdd
());
General insights:
dispatch()
and allows you to dispatch something other than actions, for example, functions or Promises. You can use Redux thunk and Redux Promise together; they are not mutually exclusive.We use reducers to shape and organize our state. When you embark on more complex applications, designing and organizing your reducers is not always trivial. In previous chapters we covered the art of designing your state and how to dissect UI state and domain specific state slices. Here are some tips that can help facilitate creating your reducers and keeping your state design clean.
General insights:
Creating a reusable initialState
const can be useful for reuse or returning back to the initial state of the reducer. A well-defined initialState
can also act as a psuedo schema for your reducer. In other words, you create your intialState
const and all possible properties with default values, allowing it to act as a point of reference for what your resulting state will look like. This is particularly useful for other devs jumping into the project, because they can glance at the initial state before diving into complex logic. Of course, sometimes this is not always possible, but it is good to do if possible. Exporting the initialState
const can also be useful to import into your tests.
export
const
initialState
=
{
// I know exactly what my reducer will change, I also export for use in my tests
isFetching
:
false
,
notes
:
[],
message
:
'Your notes list is empty, click to fetch them.'
,
numberOfVisibleNotes
:
0
,
author
:
null
//updated when user inputs into field
};
Common reducer examples use the switch statement to handle actions. This is not a requirement, as it is okay to use if statements or use other conditional logic that matches by something other than type (such as logic based on payload data).
const
initialState
=
[];
// ...
function
meta
(
state
=
initialState
,
{
type
,
payload
,
meta
})
{
if
(
meta
.
something
)
{
return
state
.
concat
(
payload
);
}
return
state
;
}
There are packages such as redux-actions
that contain helper methods to facilitate in eliminating large switch statements. In general, if you find your reducers with extremely large switch statements, consider extracting logic into helper functions or dividing your reducer into multiple smaller reducers.
redux-actions
package contains several helpers to simplify creating actions and reducers. This may be a viable addition depending on your needs. https://github.com/acdlite/redux-actions#handleactionsreducermap-defaultstate.combineReducers()
multiple times, since it returns a reducer with (state, action) => state signature, just like a standard reducer.normalizr
or redux-orm
, see the official Redux FAQ for more information: http://redux.js.org/docs/FAQ.html#organizing-state-nested-dataA selector is a function that selects part of the state tree. Selectors are also commonly used to return data that is derived from the state.
get
.Reselect
docs: https://github.com/reactjs/reselect.Middleware provides a third-party extension point between dispatching an action and the moment the action reaches the reducer.
A store enhancer is a higher-order function that composes a store creator to return a new, enhanced store creator. This is similar to middleware in that it allows you to alter the store interface in a composable way.
withExtraArgument
so you can inject extra arguments allowing you to mock your async API making writing unit tests easier. This can be useful to inject common functionality.While Redux itself has nothing to say about file structures (as it should depend on your specific needs), there are a number of common patterns for organizing the file structure of a Redux project. Each pattern has pros and cons. Let’s explore these pros and cons.
The most common way is to organize your project by concept. This is simply arranging the application files by function. Each concept lives under a common parent.
├── src# source
│ └── styles# css/sass/less
│ └── images# .png,.svg,.gif etc.
│ └── js# react/redux source
│ ├── actions# redux actions
│ ├── components# react components/containers
│ ├── constants# constants, action types
│ ├── reducers# reducers
│ ├── selectors# selectors, using reselect
│ ├── store# store, w/devtools & prod config
│ ├── utils# utilities/helpers
│ └── index.js# app entry
│ └── index.html# app shell
├──test
# tests
│ └── *.js# specs
│ └── setup.js# test config
├── webpack# webpack
├── .babelrc# babel config
├── .eslintrc# eslint config
├── .gitignore# git config
├── LICENSE# license info
├── package.json# npm
└── README.md# installation, usage
Another common way to organize your project is by arranging your files into feature folders, grouping everything related to a specific feature.
├── src# source
│ └── notes# feature
│ ├── Notes.js# component/container
│ ├── Notes.test.js# tests
│ ├── NotesActions.js# actions
│ ├── NotesActions.test.js# tests
│ ├── notesReducer.js# reducer/selectors
│ └── notesReducer.test.js# tests
│ └── comments# feature
│ ├── Comments.js# component/container
│ ├── Comments.test.js# tests
│ ├── CommentsActions.js# actions
│ ├── CommentsActions.test.js# tests
│ ├── commentsReducer.js# reducer/selectors
│ └── commentsReducer.test.js# tests
│ └── index.html# app shell
├── webpack# webpack
├── .babelrc# babel config
├── .eslintrc# eslint config
├── .gitignore# git config
├── LICENSE# license info
├── package.json# npm
└── README.md# installation, usage
Depending on your project’s complexity, you may want to choose to group your actions and reducers in one file. It is a slight variation that may help readability, particularly in smaller projects. The community already took note of such a pattern and Erik Rasmussen summarised the approach under the concept of Ducks https://github.com/erikras/ducks-modular-redux. According to him, it makes more sense for these pieces to be bundled together in an isolated, self-contained module which can be easily packaged into a library.
Provider
HOC (higher order component) wraps a root component and makes it possible to use connect()
recompose
is a handy set of React tools to optimize your components https://github.com/acdlite/recompose.onlyUpdateForKeys
from recompose
can be used to make sure only updates occur if specific prop keys have changedcompose()
as follows:import
{
connect
}
from
'react-redux'
;
import
{
compose
}
from
'redux'
;
class
MyComponent
extends
Component
{
// ...
}
const
enhance
=
compose
(
onlyUpdateForKeys
([
'propA'
,
'propB'
]),
connect
(
mapStateToProps
,
mapDispatchToProps
)
// anything else
)
export
default
enhance
(
MyComponent
)
The two most useful tools for developers and debugging Redux applications are custom logging middleware and the redux-devtools-extension
package for Chrome, Firefox, and Electron. These tools will cover your needs 99% of the time. The custom logging middleware allows you to tweak your logging as you go and has a simple, small footprint.
The redux-devtools-extension
allows for minimal configuration to enhance your Redux store and leverages the power of several existing Redux DevTools extensions. This makes it easy to implement and easier to maintain (it is not directly coupled to your application code).
Custom Logging middleware:
// logger.js
export
default
(
store
)
=>
(
next
)
=>
(
action
)
=>
{
console
.
groupCollapsed
(
action
.
type
);
console
.
info
(
'action:'
,
action
);
const
result
=
next
(
action
);
console
.
debug
(
'state:'
,
store
.
getState
());
console
.
groupEnd
(
action
.
type
);
return
result
;
};
Redux DevTools Extension: https://github.com/zalmoxisus/redux-devtools-extension
// configureStore.js
import
{
createStore
,
applyMiddleware
,
compose
}
from
'redux'
;
import
rootReducer
from
'../reducers'
;
import
thunk
from
'redux-thunk'
;
import
logger
from
'../middleware/logger'
;
const
IS_BROWSER
=
typeof
window
!==
'undefined'
;
const
IS_DEV
=
process
.
env
.
NODE_ENV
!==
'production'
;
const
middleware
=
[
thunk
];
if
(
IS_DEV
){
middleware
.
push
(
logger
);
}
export
default
function
configureStore
(
initialState
=
{})
{
return
createStore
(
rootReducer
,
initialState
,
compose
(
applyMiddleware
(...
middleware
),
IS_DEV
&&
IS_BROWSER
&&
window
.
devToolsExtension
?
window
.
devToolsExtension
()
:
(
f
)
=>
f
));
}
For additional developer tools & resources, checkout out: https://github.com/markerikson/redux-ecosystem-links/blob/master/devtools.md
initialState
value from your reducer modules to reuse in your tests.mapStateToProps()
, extract behavior from mapStateToProps()
and move into selectors, since selectors are easier to test.mapDispatchToProps()
is usually not worth testing.The tips in this chapter will help you as you implement your own Redux applications. In the next and final chapter, we explore the topic of going offline.
3.20.206.201