This project serves as a guide to structure Redux for a real world React application. Once the setup is complete you can start making api calls in no time for any entity in your system.
The Problem: Setting up Redux to work for a React app can be quite challenging and quickly result into a lot of boilerplate being repeated.
The Aim: The aim of this project is to setup Redux in such way that it will reduce the boilerplate to a minimum when adding extra entities to the system and cover most (over 90%) of our api needs.
- Demonstrate simplicity
- Understanding the Guide
- Setup
- Actions
- Middlewares
- Reducers
- Selectors
- react-redux
- Production ready
- Coming soon
- Help
After the setup the only thing we need to do to introduce a new entity (e.g. user) is to:
- Include the entity along with its nested relationships in
src/redux/index.js
- Call
getReducers
for this entity insrc/redux/reducers/index.js
With these two lines of code we can perform all the actions described in the Actions section for this entity
Then, using the react-redux containers explained later in this guide, you can start making your api calls in React in no time!
There is a Medium article explaining the core concepts of the setup, which you can find here. See the end of the article for a video of my presentation at the React London meetup on these concept or follow the link here.
I advise you to read the article before diving into the code.
You can also run yarn start
to run a demo application using this code. This relies on some mock api calls found in src/index.js
, therefore it will return predetermined data and it won't behave as a real world application. Nevertheless, it would be very useful to check the redux devtools to see how the store is structure and how it gets updated in response to different actions.
Finally, you can check the tests under src/redux/__tests__
to understand how the action->middleware->reducer + selector combination works.
Quick summary:
- Dispatch a
REQUEST
action. - Make the api call in the api middleware.
- Normalize response in the normalize middleware.
- Store payload in
byId
reducer + update status of api call in one of the other reducers.
- Access the actions and the stored payload using a Higher Order Component (connect react with redux).
All action creators, reducers and selectors will receive an entityName argument which will be any of the entities type we have in our application (e.g. user, post, comment e.t.c). This means that all of our code is generic and that we only need to write it once and then it will work for any entity in the system without extra boilerplate.
All action creators live under src/redux/actions
There are action creators for:
- Reading a single entity (e.g. GET /user/1)
- Reading multiple entities (e.g. GET /user)
- Updating a single entity (e.g. PUT /user/1)
- Updating multiple entities (e.g. PUT /user/1,2). This will probably be different in some projects so you can adjust accordingly.
- Deleting a single entity (e.g. DELETE /user/1)
- Deleting multiple entities (e.g. DELETE /user/1,2)
- Create a single entity (e.g. POST /user)
- Add an entity to another in a many to many relationship (e.g. POST /post/1/tag/1)
- Add multiple entities to another in a many to many relationship (e.g. POST /post/1/tag/1,2)
- Remove an entity from another in a many to many relationship (e.g. DELETE /post/1/tag/1)
- Remove multiple entities from another in a many to many relationship (e.g. DELETE /post/1/tag/1,2)
All actions return 4 fields:
type
. The type of the action (e.g.REQUEST_READ_USER
)params
. These are parameters that will be used by the api service to compute the api endpoint.meta
. Meta data to be used by the reducers and the normalizer middleware.options
. Extra options. Typically these can includeonSuccess
andonFail
functions to be called when the api call is done.
All middlewares live under src/redux/middlewares
.
All actions will pass by the middlewares. There are two middlewares:
- Api middleware. This is responsible for doing the api call (depending on the action type) and responding with success/fail action depending on the type of repsonse
- Normalize middleware. This will normalize the payload using the
normalizr
library and the schema provided by us.
All reducers live under src/redux/reducers
. There are 6 subreducers for every entity.
byId
. All the normalized data will be stored here.- On
SUCCESS_CREATE
the id of the created entity(ies) will be added to the parent entity. - On
SUCCESS_DELETE
the id of the deleted entity(ies) will be removed from the parent entity. - Same for
SUCCESS_REMOVE
,SUCCESS_ADD
,SUCCESS_SET
for many to many relationships.
- On
readIds
. Information about the status of all read calls will be stored here.
- On
SUCCESS_CREATE
the id of the created entity(ies) will be added to the relevant readId. - On
SUCCESS_DELETE
the id of the deleted entity(ies) will be removed from the relevant readId.
updateIds
. Information about the status of all update calls will be stored here.createIds
. Information about the status of all create calls will be stored here.deleteIds
. Information about the status of all delete calls will be stored here.toggleIds
. Information about the status of all toggle calls will be stored here. Toggle refers to remove/add one entity to another in a many to many relationship.
Since the data is stored in a normalized structure it becomes very easy to update relational data. Consider the following example where the initial state:
{
entities: {
user: {
1: {
id: 1,
posts: [1,2],
}
}
}
}
If we create a post (it will receive the id 3) then in the byId
reducer we can add the id to the posts
array under the parent entity (in this case user). The new state will become:
{
entities: {
user: {
1: {
id: 1,
posts: [1,2. 3],
}
}
}
}
Note that there are two ways to retrieve the posts for a user. We could either load the user and return posts as nested data from our backend, which would lead to the initial state above. Or we might want to return the posts for a specific user_id (Usually the case when we paginate data). In this case the initial state would look like this:
{
entities: {
post: {
'{"user_id":1}': { items: [1,2] },
}
}
}
And the updated state:
{
entities: {
post: {
'{"user_id":1}': { items: [1,2, 3] },
}
}
}
All these are handle automatically and for all entities, so we don't have to worry about updating relationships anymore.
All selectors live under src/redux/selectors
. The selectors will select either the data from the byId
reducer and denormalize it or the status of the operation from the readIds
, updateIds
, createIds
, deleteIds
and toggleIds
reducers.
All logic for connecting redux and react components live under src/react-redux
. The mapDispatchToProps and mapStateToProps is moved in to higher order components so that we don't need to redeclare them in every component. You can see how these HOC are used in the example in src/components
.
Example to read a single entity:
<ReadSingleEntityContainer entityName='user' id={1}>
{ props => <MyComponent {...props} /> }
</ReadSingleEntityContainer>
- Wrap your component around the HOC.
- Pass the entityName and id props to the HOC.
- You get access to the
read
action creator, theentity
(user) that will be returned from the api call, andstatus
(isFetching, error).
See src/components/Main/index.js
for the full example.
This setup is the basis for the Redux setup at Labstep. It is used in production and has accelerated the development drastically.
TODO:
- Add examples for cursor/page based read
- Add example for caching / optimistic updates
- Publish to npm (I plan to turn this into a package that everyone can use )
Feel free to open an issue asking for help. I'll do my best to reply promptly.