react, redux, redux-saga, rotuer5, reselect, normalizr
git clone git@github.com:salsita/redux-boilerplate.git
cd redux-boilerplate
yarn
npm start
Navigate your browser to http://localhost:3000/
For now, we have agreed on using:
react
for renderingredux
for state managementrouter5
for client-side routingredux-saga
for business logic and asynchronous workflowreselect
for memoized, composable selectorsnormlizr
for normalizing business entities
The heart of rendering is react
. The goal is to keep react
as very simple thin rendering library and therefore delegate state management to redux
. However, sometimes it makes sense to keep the Component
stateful, for example: a lot of DOM interaction, need for Component lifecycle, non-business specific simple state management (e.g. hover
).
You are strongly encouraged to read & use this styleguide, feel free to file an issue if you disagree with some rule, or you feel like adding a new one.
- Prefer predefined directory structure, which is flat, we don't want nested folders because it makes reasoning about imports much more difficult
- File name must be unique across the whole project, it's good practice to add suffix of file type eg.
counterSaga
andcounterReducer
- Only classes or
React
Components (keep in mind that Containers are alsoReact
Components) should have the first letter of their filename capitalized
import library from 'library';
import nextLibrary, { namedStuffFromNextLibrary } from 'next-library';
import { namedStuffFromLibrary } from 'another-library';
import DefaultImport from 'src/components/Component';
import * as ActionTypes from 'src/constants/actionTypes';
import * as Whatever from 'src/whatever/whatever';
- Prefer wildcard imports over named, because it namespaces variables in the scope
- Use aliases instead of relative imports
- If possible order group of imports alphabetically by path
-
Keep all your
react
components & containers withinsrc/components
folder, use.js
suffix even though you are technically using JSX -
Always name your component before exporting, therefore prefer this:
import React from 'react';
const MyComponent = () => <div>Hello World</div>;
export default MyComponent;
over this:
import React from 'react';
export default () => <div>Hello World</div>;
There are two reasons: the component will have a name in react
devtools, and it's much easier to add proptypes later even when the component does not currently accept any props
-
Always specify PropTypes and be as specific as possible, therefore using of
PropTypes.array
is banned, usePropTypes.shape
(wherever possible instead ofPropTypes.object
) andPropTypes.arrayOf
instead -
Keep in mind that 99% of your props are
required
so don't forget to define them asisRequired
in the definition -
Prefer destructured props over accessing them
-
Feel free to use Short-circuit evaluation
-
Import
React
first, then define the component, then specify PropTypes and as the last step do default export -
Always use
default
exports for Components -
Split Component into many sub components when the Component becomes too complex, keep them in the same file if it makes sense, otherwise you can make it generic and abstract it away to separate exported
Component
-
Never ever use
bind
or lambda functions in event handlers, they create new reference with each render! Of course an exception is when you need to pass an argument (e.g. index of item which has been clicked), however, think twice if that's really the case -
Use stateful components if it makes sense (simple UI state, component lifecycle, heavy DOM manipulation)
-
Never rely on
dispatch
presence directly in the Component, prefer action creators.onClick={() => dispatch({ type: 'FOO' })}
is simply wrong because it creates new function with each render
- Use uppercase convention for all the constants
- Action types are always called
actionTypes
notactions
! - Try to logically group
actionTypes
together by using empty lines as visual separators - Might even be a good idea to annotate the group with comments
-
Keep them in the
src/components
folder -
For
mapDispatchToProps
usebuildActionCreators
helper. The helper accepts an object where keys are prop names and values types of actions to be dispatched, it automatically generates action creators which dispatch the action of specified type and payload passed to the function
const { onClick } = buildActionCreators({
onClick: 'CLICKED'
});
// is equal to:
const onClick = dispatch => payload => dispatch({ 'CLICKED', payload });
mapDispatchToProps({ onClick });
- Keep in mind that
connect
has also the third argument called mergeProps which may be very handy in some situations
E.g. tag actions by instance id of the Container (Elmish approach):
const CounterContainer = connect(
mapStateToProps,
buildActionCreators({
onIncrement: ActionTypes.INCREMENT
}),
(stateProps, dispatchProps, ownProps) => ({
...ownProps,
...stateProps,
...dispatchProps,
onIncrement: () => dispatchProps.onIncrement(ownProps.counterId)
})
)(Counter);
<CounterContainer counterId='topCounter' />
<CounterContainer counterId='bottomCounter' />
Re-shaping dispatched action:
const ControlledTextField = ({ onChange, value }) => <input type="text" onChange={onChange} value={value} />;
const ControlledTextFieldContainer = connect(
mapStateToProps,
buildActionCreators({
onChange: ActionTypes.TEXT_FIELD_CHANGE
}),
(stateProps, dispatchProps, ownProps) => ({
...ownProps,
...stateProps,
...dispatchProps,
onChange: ev => dispatchProps.onChange(ev.target.value)
})
)(ControlledTextField)
- Never use
yield*
, always preferyield call
oryield fork
function* apiSaga() {
yield put({ type: 'SetLoadingSpinner' });
try {
yield call(api);
} finally {
yield put({ type: 'ResetLoadingSpinner' });
}
}
// You better do this
function* rootSaga() {
yield take('CallAPI');
// You should realize that you can choose between call and fork
// depending on use case
yield call(apiSaga);
}
// Instead of this
function* rootSaga(){
yield take('CallAPI');
yield* apiSaga();
}
The reason why call
is preferred way is because of testing
- Always
default
export Saga and fork the function in the parent. Therefore if you want totakeEvery
you can do that in the exported function for particular saga. SeehelloUserSaga
There is a basic setup with stylus
and autoprefixer
, in development the styles are embedded right into the component, in production the styles are extracted using extract-text-webpack-plugin
to standalone CSS file.
The code is linted using eslint
, we extend airbnb
's code style.
One especially handy plugin is eslint-plugin-import
, its responsibility is checking whether imported modules really exists in the file system. It works for libraries & user modules as well.
There are two scripts available:
npm run test
for single test runnpm run test:watch
for watching changes and re-runnig the tests
Testing framework is Jest, there's no configuration and the only command that is used is jest
. It automatically uses babel
(configured via .babelrc
) for transpiling.
We are using now.sh for realtime global deployment, all you have to do to get your current application online is running npm run deploy
. First time you run the command, you will be prompted for e-mail, just provide the e-mail and then visit the confirmation link you get. From then on, you can deploy the app by running npm run deploy
and the application gets deployed on random URL (which is going to be copied to your cliboard).
Implementation is easy, all we had to do was install now
via npm
. Deploy script runs the build script which creates static assets inside dist/
folder. This folder contains the only non-gitignored file which is package.json
and this package.json
is responsible for running static HTTP server. After the application is built, it's just a matter of now dist/
to get the application online.