Skip to content

Commit 5c19204

Browse files
authored
Merge pull request onoufriosm#2 from onoufriosm/refactoring
Refactoring
2 parents 7cc7bc6 + 7d0b2ee commit 5c19204

File tree

25 files changed

+776
-261
lines changed

25 files changed

+776
-261
lines changed

README.md

Lines changed: 121 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,84 @@
11
# State management with Redux
22

3-
This project serves as a guide to structure Redux for a react app. The goal is to setup Redux in such way that it will cover most (over 90%) of your api needs.
3+
This project serves as a guide to structure Redux for a react app.
44

5-
> #### _Like this guide?_ **Show your support by giving a :star:**
5+
**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.
6+
7+
**The Aim:** The aim of this project is to setup Redux in such way that it will reduce the boilerplate to a minimum and cover most (over 90%) of our api needs.
68

7-
**Note:** This code is nearly complete (See [Coming soon](#coming-soon)). It is functional and can be used as is or serve as inspiration. Some actions (delete, create) for multiple entities might not work as expected yet.
9+
> #### _Like this guide?_ **Show your support by giving a :star:**
810
911
---
1012

11-
## Table of Contents
13+
## Docs
14+
- [Understanding the Guide](#understanding)
1215
- [Setup](#setup)
1316
- [Actions](#actions)
1417
- [Middlewares](#middlewares)
1518
- [Reducers](#reducers)
1619
- [Selectors](#selectors)
1720
- [react-redux](#react-redux)
21+
- [Production ready](#production)
1822
- [Coming soon](#coming-soon)
23+
- [Help](#help)
1924

2025
---
2126

27+
## Understanding the Guide
28+
29+
There is a Medium article explaining the core concepts of the setup, which you can find [`here`](https://medium.com/@onoufriosm/state-management-with-redux-50f3ec10c10a). 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`](https://www.youtube.com/watch?time_continue=3231&v=yElOj4R4rdA).
30+
31+
I advise you to read the article before diving into the code.
32+
33+
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.
34+
35+
Finally, you can check the tests under `src/redux/__tests__` to understand how the action->middleware->reducer + selector combination works.
36+
37+
2238
## Setup
2339

24-
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.
40+
Quick summary:
41+
1. Dispatch a `REQUEST` action.
42+
2. Make the api call in the api middleware.
43+
3. Normalize response in the normalize middleware.
44+
4. Store payload in `byId` reducer + update status of api call in one of the other reducers.
45+
+ Access the actions and the stored payload using a Higher Order Component (connect react with redux).
2546

47+
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.
2648

2749
## Actions
2850

2951
All action creators live under `src/redux/actions`
3052

53+
All actions return 4 fields:
54+
1. `type`. The type of the action (e.g. `REQUEST_READ_USER`)
55+
2. `params`. These are parameters that will be used by the api service to compute the api endpoint.
56+
3. `meta`. Meta data to be used by the reducers and the normalizer middleware.
57+
4. `options`. Extra options. Typically these can include `onSuccess` and `onFail` functions to be called when the api call is done.
58+
3159
There are action creators for:
3260
1. Reading a single entity (e.g. GET /user/1)
3361
2. Reading multiple entities (e.g. GET /user)
3462
3. Updating a single entity (e.g. PUT /user/1)
3563
4. Updating multiple entities (e.g. PUT /user/1,2). This will probably be different in some projects so you can adjust accordingly.
3664
5. Deleting a single entity (e.g. DELETE /user/1)
37-
6. Create a single entity (e.g. POST /user)
38-
7. Add an entity to another in a many to many relationship (e.g. POST /post/1/tag/1)
39-
7. Remove an entity to another in a many to many relationship (e.g. DELETE /post/1/tag/1)
65+
6. Deleting multiple entities (e.g. DELETE /user/1,2)
66+
7. Create a single entity (e.g. POST /user)
67+
8. Add an entity to another in a many to many relationship (e.g. POST /post/1/tag/1)
68+
9. Add multiple entities to another in a many to many relationship (e.g. POST /post/1/tag/1,2)
69+
10. Remove an entity from another in a many to many relationship (e.g. DELETE /post/1/tag/1)
70+
11. Remove multiple entities from another in a many to many relationship (e.g. DELETE /post/1/tag/1,2)
4071

4172
[⇧ back to top](#table-of-contents)
4273

4374
## Middlewares
4475

45-
All middlewares live under `src/redux/middlewares`.There are two middlewares:
76+
All middlewares live under `src/redux/middlewares`.
77+
78+
All actions will pass by the middlewares. There are two middlewares:
4679

47-
1. Api middleware. This is responsible for doing the api call and responding with success/fail action depending on the type of repsonse
48-
2. Normalize middleware. This will normalize the payload using the [`normalizr`](https://github.com/paularmstrong/normalizr) library.
80+
1. 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
81+
2. Normalize middleware. This will normalize the payload using the [`normalizr`](https://github.com/paularmstrong/normalizr) library and the schema provided by us.
4982

5083
[⇧ back to top](#table-of-contents)
5184

@@ -54,12 +87,71 @@ All middlewares live under `src/redux/middlewares`.There are two middlewares:
5487
All reducers live under `src/redux/reducers`. There are 6 subreducers for every entity.
5588

5689
1. `byId`. All the normalized data will be stored here.
90+
- On `SUCCESS_CREATE` the id of the created entity(ies) will be added to the parent entity.
91+
- On `SUCCESS_DELETE` the id of the deleted entity(ies) will be removed from the parent entity.
92+
- Same for `SUCCESS_REMOVE`, `SUCCESS_ADD`, `SUCCESS_SET` for many to many relationships.
5793
2. `readIds`. Information about the status of all read calls will be stored here.
94+
- On `SUCCESS_CREATE` the id of the created entity(ies) will be added to the relevant readId.
95+
- On `SUCCESS_DELETE` the id of the deleted entity(ies) will be removed from the relevant readId.
5896
3. `updateIds`. Information about the status of all update calls will be stored here.
5997
4. `createIds`. Information about the status of all create calls will be stored here.
6098
5. `deleteIds`. Information about the status of all delete calls will be stored here.
6199
6. `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.
62100

101+
Since the data is stored in a normalized it becomes very easy to update relational data. Consider the following example where the initial state:
102+
```
103+
{
104+
entities: {
105+
user: {
106+
1: {
107+
id: 1,
108+
posts: [1,2],
109+
}
110+
}
111+
}
112+
}
113+
```
114+
115+
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:
116+
117+
```
118+
{
119+
entities: {
120+
user: {
121+
1: {
122+
id: 1,
123+
posts: [1,2. 3],
124+
}
125+
}
126+
}
127+
}
128+
```
129+
130+
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:
131+
132+
```
133+
{
134+
entities: {
135+
post: {
136+
'{"user_id":1}': { items: [1,2] },
137+
}
138+
}
139+
}
140+
```
141+
142+
And the updated state:
143+
```
144+
{
145+
entities: {
146+
post: {
147+
'{"user_id":1}': { items: [1,2, 3] },
148+
}
149+
}
150+
}
151+
```
152+
153+
All these are handle automatically and for all entities, so we don't have to worry about updating relationships anymore.
154+
63155
[⇧ back to top](#table-of-contents)
64156

65157
## Selectors
@@ -70,7 +162,7 @@ All selectors live under `src/redux/selectors`. The selectors will select either
70162

71163
## react-redux
72164

73-
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 use in the example in `src/components`.
165+
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`.
74166

75167
Example to read a single entity:
76168
```
@@ -83,19 +175,28 @@ Example to read a single entity:
83175
2. Pass the entityName and id props to the HOC.
84176
3. You get access to the `read` action creator, the `entity` (user) that will be returned from the api call, and `status` (isFetching, error).
85177

178+
See `src/components/Main/index.js` for the full example.
179+
180+
[⇧ back to top](#table-of-contents)
181+
182+
## Production ready
183+
184+
This setup is the basis for the Redux setup at [`Labstep`](https://www.labstep.com/). It is used in production and has accelerated the development drastically.
185+
86186
[⇧ back to top](#table-of-contents)
87187

88188
## Coming soon
89189

90190
TODO:
91191

92-
1. Fix + make uniform create, delete, toggle for multiple entities
93-
2. Finish writing unit tests
94-
3. Write examples for cursor/page based read
95-
4. Add caching
96-
5. Add optimistic updates
97-
6. Allow for rxjs/saga replacement
98-
7. Finish writing documentation
99-
8. Publish to npm (I plan to turn this into a package that everyone can use )
192+
1. Add examples for cursor/page based read
193+
2. Add example for caching / optimistic updates
194+
3. Publish to npm (I plan to turn this into a package that everyone can use )
195+
196+
[⇧ back to top](#table-of-contents)
197+
198+
## Help
199+
200+
Feel free to open an issue asking for help. I'll do my best to reply promptly.
100201

101202
[⇧ back to top](#table-of-contents)

package.json

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
{
2-
"name": "state",
3-
"version": "0.1.0",
4-
"private": true,
2+
"name": "redux-setup-guide",
3+
"version": "0.2.0",
4+
"description": "Guide on how to setup Redux for a react application",
5+
"license": "MIT",
6+
"author": "Onoufrios Malikkides",
7+
"keywords": [
8+
"redux",
9+
"react",
10+
"state",
11+
"guide"
12+
],
513
"dependencies": {
614
"antd": "^3.11.2",
715
"axios": "^0.18.0",

src/index.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,32 @@ import { Provider } from 'react-redux';
44
import AppLayout from './components/Layout';
55
import * as serviceWorker from './serviceWorker';
66
import setupStore from './redux';
7+
import axios from 'axios';
8+
import MockAdapter from 'axios-mock-adapter';
9+
10+
// Mock api calls
11+
const axiosMock = new MockAdapter(axios, { delayResponse: 500 });
12+
axiosMock.onGet('http://localhost:8000/user/1').reply(200, {
13+
id: 1,
14+
name: 'John',
15+
posts: [{ id: 1, tags: [{ id: 1, name: 'important' }] }],
16+
});
17+
axiosMock.onGet('http://localhost:8000/tag').reply(200, [
18+
{ id: 1, name: 'important' },
19+
{ id: 2, name: 'serious' },
20+
{ id: 3, name: 'ready' },
21+
]);
22+
axiosMock.onPut('http://localhost:8000/user/1').reply(200, {
23+
id: 1,
24+
name: 'James',
25+
});
26+
axiosMock.onPost('http://localhost:8000/post').reply(200, {
27+
id: 20,
28+
text: 'My newly created post',
29+
tags: [],
30+
});
31+
axiosMock.onDelete('http://localhost:8000/post/1').reply(200,{});
32+
733

834
const store = setupStore({}, { debug: true });
935

src/react-redux/Entity/Create/index.js

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
/* Dependencies */
6+
import React from 'react';
67
import { connect } from 'react-redux';
78
import { v4 } from 'uuid';
89

@@ -14,27 +15,68 @@ import { selectCreatedEntity, selectCreateEntityStatus } from '../../../redux/se
1415

1516
const Container = ({ children, ...rest }) => children(rest);
1617

17-
let uuid = null;
18-
1918
const mapStateToProps = (state, ownProps) => ({
20-
createdEntity: selectCreatedEntity(state, ownProps.entityName, uuid),
21-
status: selectCreateEntityStatus(state, ownProps.entityName, uuid),
19+
createdEntity: selectCreatedEntity(state, ownProps.entityName, ownProps.uuid),
20+
status: selectCreateEntityStatus(state, ownProps.entityName, ownProps.uuid),
2221
});
2322

2423
const mapDispatchToProps = (dispatch, ownProps) => ({
25-
create(body, options) {
26-
uuid = v4();
24+
create(body, options = {}) {
25+
const enhancedOptions = {
26+
...options,
27+
onSuccess: () => {
28+
if (options.onSuccess) {
29+
options.onSuccess();
30+
}
31+
// We need to get a new uuid on success
32+
ownProps.refreshUuid();
33+
},
34+
};
2735
dispatch(
2836
createEntity(
2937
ownProps.entityName,
3038
ownProps.parentName,
3139
ownProps.parentId,
32-
uuid,
40+
ownProps.uuid,
3341
body,
34-
options,
42+
enhancedOptions,
3543
),
3644
);
3745
},
3846
});
3947

40-
export default connect(mapStateToProps, mapDispatchToProps)(Container);
48+
49+
// Here we are passing a unique id to the children to keep track of the operation as there
50+
// is not id that we could use before the entity get created.
51+
const ConnectedChildren = connect(
52+
mapStateToProps,
53+
mapDispatchToProps,
54+
)(Container);
55+
56+
export class UuidContainer extends React.Component {
57+
constructor(props) {
58+
super(props);
59+
this.state = {
60+
uuid: v4(),
61+
};
62+
this.refreshUuid = this.refreshUuid.bind(this);
63+
}
64+
65+
refreshUuid() {
66+
this.setState({ uuid: v4() });
67+
}
68+
69+
render() {
70+
const { uuid } = this.state;
71+
72+
return (
73+
<ConnectedChildren
74+
{...this.props}
75+
refreshUuid={this.refreshUuid}
76+
uuid={uuid}
77+
/>
78+
);
79+
}
80+
}
81+
82+
export default connect(mapStateToProps, mapDispatchToProps)(UuidContainer);

src/react-redux/Entity/Read/Entities/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ import { connect } from 'react-redux';
99
import { readEntities } from '../../../../redux/actions';
1010

1111
/* Selectors */
12-
import { selectEntities, selectReadEntitiesStatus } from '../../../../redux/selectors';
12+
import { selectReadEntities, selectReadEntitiesStatus } from '../../../../redux/selectors';
1313

1414
const Container = ({ children, ...rest }) => children(rest);
1515

1616
const mapStateToProps = (state, ownProps) => ({
17-
entities: selectEntities(state, ownProps.entityName, ownProps.params),
17+
entities: selectReadEntities(state, ownProps.entityName, ownProps.params),
1818
status: selectReadEntitiesStatus(state, ownProps.entityName, ownProps.params),
1919
});
2020

src/react-redux/Entity/Toggle/index.js

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { connect } from 'react-redux';
77

88
/* ACTIONS */
9-
import { addEntity, removeEntity, setEntity } from '../../../redux/actions';
9+
import { addEntity, removeEntity } from '../../../redux/actions';
1010

1111
/* Selectors */
1212
import { selectToggleEntityStatus } from '../../../redux/selectors';
@@ -56,16 +56,6 @@ const mapDispatchToProps = (dispatch, ownProps) => ({
5656
options,
5757
),
5858
);
59-
} else if (actionType === 'set') {
60-
dispatch(
61-
setEntity(
62-
ownProps.entityName,
63-
entityIds,
64-
ownProps.parentName,
65-
ownProps.parentId,
66-
options,
67-
),
68-
);
6959
}
7060
},
7161
});

0 commit comments

Comments
 (0)