My Approach to React & Redux

Page Header

My first foray into React and Redux was pretty terrible. I had started working on a project that had a bad implementation, and I hated it.

I also found Redux to be strange and pointless to begin with, feeling like it was just unnecessary repetition for defining data and functions. It didn’t help that most of my prior JS experience was some mild jQuery with ASP.NET, so I was still coming up to speed with the SPA & React paradigms. The thing that truly clued me into it’s value was trying to make a React application without Redux.

My greatest takeaway with developing with React is that fighting against how they want you to build it never works. Just go with the flow.

Project Creation

I use react-create-app to start my React projects. Then, I install the following packages:

1
2
3
4
5
6
7
react-dom
react-helmet
react-redux
react-router
react-router-dom
redux
redux-thunk

Project Layout

Just to give some context for the next sections, this is how my projects end up being laid out:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
actions\
actions\actionTypes.js
components\
containers\
css\
fonts\
img\
lib\
reducers\
reducers\rootReducer.js
reducers\initialState.js
app.js
index.html
main.js
routes.js
store.js

I don’t really make subfolders for each component or anything. I found the React Boilerplate takes this approach, but I found that approach over-engineered. The approach I use now feels a lot more natural.

I use the lib\ folder for anything that isn’t React/Redux related. E.g. I keep a consts.js file in there for global constants, and an api.js for any calls to external services I make, amongst other things.

Containers

This is usually how I make a container. The container renders a matching Page component, and passes through button event handlers, the props, and any local state.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import * as fooActions from '../actions/fooActions';

class Foo extends React.Component {
state = {
someLocalStateVariable: '',
}

handleClickButton = (e) => { this.props.fooActions.doSomething() }

render() {
return (
<FooPage
handleClickButton={this.handleClickButton}
someReduxProp={this.props.foo.someReduxProp}
someLocalStateVariable={this.state.someLocalStateVariable}
/>
)
}
}

Foo.propTypes = { {/* Add your Prop Types here... */} } };

const mapStateToProps = (state) => ({ foo: state.foo });
const mapDispatchToProps = (dispatch) => ({ fooActions: bindActionCreators(fooActions, dispatch) });

export default connect(mapStateToProps, mapDispatchToProps)(Foo);

This is fundamentally all I do in these containers. If I start creating some event handler and writing an if statement inside of it, then I know I’m probably doing it wrong and need to offload that work to an action instead.

Components

Components must be dumb. You can guarantee that by not even declaring them as a class but by just returning JSX:

1
2
3
4
5
6
7
8
9
10
11
const FooPage = (props) => (
<Segment>
<Heading as="h2" content="Foo Heading" />
{props.someReduxProp && (
<Header as="h3" content="Some Redux Prop Present" />
)}
{props.someLocalStateVariable}
</Segment>
)

export default FooPage;

That Segment Component - that comes from Semantic UI. If I have a “Page” component exceed a couple of hundred lines of code, then I usually break up bits of that into subcomponents for better readability.

Actions

I’ve found the majority of actions are just:

  1. Start to do something async (“REQUESTED”)
  2. Finish doing something async (“SUCCESS”)
  3. Catch any failures (“FAILED”)

Which is what mose of my actions end up looking like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import * as types from './actionTypes';
import * as api '../lib/api';

export const doFoo = () =>
(dispatch) => {
dispatch({
type: types.FOO_DO_FOO_REQUESTED,
});

api.doFoo()
.then((response) => {
dispatch({
type: types.FOO_DO_FOO_SUCCESS,
data: response,
});
})
.catch((error) => {
dispatch({
type: types.FOO_DO_FOO_FAILED,
data: error,
});
});
};

I prefix each action type with the container related to them. Since these actions are called by the FooContainer, they’re all prefixed with FOO_.

Of course I do end up in some cases add more dispatch types for some methods, e.g. to handle different error response codes.

Action Types

Just a big ol’ file filled with exported consts:

1
2
3
export const FOO_DO_FOO_REQUESTED = 'FOO_DO_FOO_REQUESTED';
export const FOO_DO_FOO_SUCCESS = 'FOO_DO_FOO_SUCCESS';
export const FOO_DO_FOO_FAILED = 'FOO_DO_FOO_FAILED';

Reducers

The reducers. I keep the initialState in a separate file and import it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import initialState from './initialState';
import * as types from '../actions/actionTypes';

function fooReducer(state = initialState.foo, action) {
switch (action.type) {
case types.FOO_DO_FOO_REQUESTED:
return {
...state,
isLoadingFoo: true,
showError: false,
};
case types.FOO_DO_FOO_SUCCESS:
return {
...state,
isLoadingFoo: false,
showError: false,
bar: action.data,
};
case types.FOO_DO_FOO_FAILED:
return {
...state,
isLoadingFoo: false,
showError: true,
errorMessage: action.data,
};
default:
return state;
}
};

export default fooReducer;

Root Reducer

1
2
3
4
5
6
7
8
import { combineReducers } from 'redux';
import fooReducer from './fooReducer';

const rootReducer = combineReducers({
foo: fooReducer,
});

export default rootReducer;

Store

Pretty straightforward, just adding in thunk for async actions:

1
2
3
4
5
6
7
8
9
10
11
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/rootReducer';

export default function configureStore() {
return createStore(
rootReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
applyMiddleware(thunk),
);
}

Linting

One last note - I use a linter to enforce consistent style. It drove me nuts for a few weeks and then I got used to it.

This is my .eslintrc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
{
"parser": "babel-eslint",
"extends": "airbnb",
"env": {
"browser": true,
"node": true,
"jest": true,
"es6": true
},
"plugins": [
"react",
"jsx-a11y"
],
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"rules": {
"arrow-parens": [
"error",
"always"
],
"arrow-body-style": [
2,
"as-needed"
],
"class-methods-use-this": 0,
"comma-dangle": [
2,
"always-multiline"
],
"import/imports-first": 0,
"import/newline-after-import": 0,
"import/no-dynamic-require": 0,
"import/no-extraneous-dependencies": 0,
"import/no-named-as-default": 0,
"import/no-unresolved": 2,
"import/no-webpack-loader-syntax": 0,
"import/prefer-default-export": 0,
"indent": [
2,
2,
{
"SwitchCase": 1
}
],
"jsx-a11y/aria-props": 2,
"jsx-a11y/heading-has-content": 0,
"jsx-a11y/href-no-hash": 0,
"jsx-a11y/anchor-is-valid": 0,
"jsx-a11y/click-events-have-key-events": 0,
"jsx-a11y/no-static-element-interactions": 0,
"jsx-a11y/label-has-for": 2,
"jsx-a11y/mouse-events-have-key-events": 2,
"jsx-a11y/role-has-required-aria-props": 2,
"jsx-a11y/role-supports-aria-props": 2,
"max-len": 0,
"newline-per-chained-call": 0,
"no-confusing-arrow": 0,
"no-console": 1,
"no-use-before-define": 0,
"prefer-template": 2,
"react/forbid-prop-types": 0,
"react/jsx-first-prop-new-line": [
2,
"multiline"
],
"react/jsx-filename-extension": 0,
"react/jsx-no-target-blank": 0,
"react/require-default-props": 0,
"react/require-extension": 0,
"react/self-closing-comp": 0,
"require-yield": 0,
"linebreak-style": [ 2, "unix"]
},
"settings": {
"import/resolver": {
"webpack": {
"config": "./internals/webpack/webpack.prod.babel.js"
}
}
}
}

I do have the occasional inline ignore, but I try to keep those down to a minimum.