Example project. We are using it as a start point for out applications or as a base for an education of our developers.
Command | Description |
---|---|
yarn dev |
Run dev server |
yarn production |
Run production server |
yarn lint |
Check code with Eslint and Stylelint |
yarn build |
Build production |
yarn stats |
Run webpack statistic dashboard to analyze bundle size` |
yarn locales:extract |
Extract locales from the app into .pot file |
yarn storybook |
Run storybook |
yarn test:acceptance |
Run acceptance tests |
app
- our application files. React JS app is here.storybook
- storybook configuration files.bin
- CI automation scriptscypress
- acceptance testsdocs
- docs related documents, assetspublic
- public static data, like favicon, robot.txt, that is not using in react js app and that doesn't have to go thought webpack processwebpack
- webpack configuration parts
Inside app
folder:
client
- client's entrypointserver
- server's entrypointcommon
- code, that is shared between client and servercommon/components
- generic components. Core UI of our application. Elements, that can be publish and reused in other projects.common/containers/blocks
- not generic components. specific for this project. API driven and can't be re-used in other projectscommon/containers/forms
- application's formscommon/containers/layouts
- layoutscommon/containers/pages
- pagescommon/helpers
- helpers.common/locales
- localization files and templatecommon/redux
- redux modules. reducers and actions are herecommon/routes
- routes definitions.common/schemas
- normalizr schemas.common/services
- configuration of 3rd party modules and servicescommon/store
- configuration of redux storecommon/styles
- shared styles, like variables
We're using eslint to keep js and jsx code style consistent. Check that your IDE is using Eslint file from this repo. Anyway, pre-commit hook is checking lint with our internal installed eslint version. So if your IDE is not showing you the errors, but you have them in pre-commit hook, re-check IDE :D
Add plugin linter-eslint. Go to the plugin's configuration and enable option Fix errors on save
Stylelint is using to control the codestyle in style files. Configure your IDE to use config from this repo.
- Stable branch -
master
- Don't push directly to the stable branch. Use PRs instead
Workflow:
- Start a ticket with a new branch
- Write code
- Create Pull Request
- Get an approve from one of your coworkers
- Merge PR's branch with the stable branch
We are not following some strict rule on this stage of branch naming. So we have a single rule for the branch names:
- Make you branch names meaningful.
Bad example
fix-1
redesign
Good example
fix-signals-table
new-user-profile-page
If you are using JIRA as a task manager, follow this naming convention
[type of ticket]/[number of branch]-[short-title]
feature/FRB-123-change-titles
fix/FRB-431-retina-images
We are creating React application with component-approach. This means, what we try to decompose our UI into re-usable parts.
Try to make components PURE when it's possible. Avoid using redux or inner state for components in app/common/components
.
Base component contains next files:
index.js
- base componentstyles.scss
- styles fileindex.story.js
- storybook file
See the example component.
Use recompose to create logic layout of your components. recompose allow us to split logic into multiple re-usable parts.
Storybook is using as a UI library. We are using it as documentation for our UI. The goals are:
- help teammates to find the right component
- help to understand how to use the component
- avoid duplications
The rule is: write a story for all generic pure components and blocks and show all the existing variations.
Help your teammates understand from story how to use your component. Not just write the story of itself. Think about your colleagues.
Shortly about our styles:
- CSS modules
- PostCss with SCSS like style
We're using scoped styles, so you don't need to use BEM or other methodology to avoid conflicts in the styles. In BEM terminology, you don't have to use elements. Use only block and modificators.
If you feel that you also need an element - think, probably you have to extract a new component from this template.
Bad example
.page {
&__title {}
&__content {
&_active {}
}
}
Good example
.root {} // root element
.title {}
.content {
&.isActive {}
}
Use is
prefix for the modificators.
We are using isomorphic style loader
to calculate critical CSS path. Use withStyles HOC to connect component and style. Use withStyles
alias for import.
For example
import React from 'react';
import PropTypes from 'prop-types';
import withStyles from 'withStyles';
import { compose } from 'recompose';
import styles from './styles.scss';
const Poster = ({ src, title }) => (
<div className={styles.root} style={{ backgroundImage: `url(${src})` }} alt={title} />
);
Poster.propTypes = {
children: PropTypes.node,
};
export default compose(
withStyles(styles)
)(Poster);
Make names meaningful.
<Button d />
Good example
<Button disabled />
Make property for group of variants, not only of one specific case
Bad example
<Button red />
<Button blue />
<Button green />
Good example
<Button color="red" />
<Button color="blue" />
<Button color="green" />
Make property short
Bad example
<Button withLongValue />
Good example
<Button long />
Use on
prefix for callbacks
Bad example
<Component change={someCallback} />
Good example
<Component onChange={someCallback} />
true
if present
<Component black />
equals <Component black={ true } />
false
if missed
<Component />
equals <Component black={ false } />
Use component fully as a block. Don't make the components styles configurable outside. It has to have the deterministic number of possible variants.
Bad example
<Component className="newClassname" />
This is a chore. Passing classname or style from parent component can solve a problem easily in short terms, but in the future, you will be updating your components and you will not remember about this modification. so you will not test it. and it would be a bug.
Good example
<div className={styles.wrapper}>
<Component disabled />
</div>
We are using redux as our global state manager in the app. Our base store structure is:
data
- data redux modules.form
- connect redux-formrouting
- connect react-router-reduxui
- reducers of UI components
Actions and reducers are stored in the same file. We are using redux-actions
as a single way to create an action. See example.
Selectors are stored in redux/index.js file. Basic set of data selectors contains next methods:
export const getMovie = (state, id) => denormalize(id, schemas.movie, state.data);
export const getMovies = (state, ids) => ids.map(id => getMovie(state, id));
export const getAllMovies = state => getMovies(state, Object.keys(state.data.movies));
We are using redux-api-middleware to make the API calls. It's use universal fetch internally.
Use method invoke
to create API request actions.
We use redux as a caching layer. Redux store contains data object, that has next structure
example
data: {
movies: {
1: {
id: 1,
name: 'The Shawshank Redemption'
},
2: {
id: 2,
name: 'The Godfather'
},
},
directors: {
1: {
id: 1,
name: 'Francis Ford Coppola'
}
}
}
So all the data entities are grouped by collection and is stored in map, there key is id
of the entity and value
is entity itself.
This structure allows us to easily find entity in store by id.
Each API request contains normalization of the response. Example.
To normalize data we are using normalizr
package. Normalization schemas are store in schemas
folder.
See an example of fetching and normalization of the data here.
Use connect
and selectors
to get the store data on the page. Don't store the data directly on the page. Store identifier and get the value by id, with selector instead.
For example,
export default compose(
withStyles(styles),
translate(),
withRouter,
provideHooks({
fetch: ({ dispatch, params, setProps }) => dispatch(fetchMovie(params.id)).then((response) => {
setProps({
movieId: response.payload.result,
});
}),
}),
connect((state, ownProps) => ({
movie: getMovie(state, ownProps.movieId),
})),
)(MoviesDetailsPage);
After the fetch of the data we don't store the whole object, but just the id of the movie and then we get whole object with getMovie selector. It allow us to be sure, that redux store is only one source of data in our applications. It helps to avoid many bugs.
- You have to be able to load page information based only on the URL params.
- Use the same URL as a main API endpoint of the page.
Page URL: /users
API request: get:/users
Page URL: /users/:id
API request: get:/users/:id
Page URL: /users/new
API request: post:/users
- If you need to save the state of the page - use the URL query params.
e.g /users?page=1, /users?q=John Doe
- Make URLs meaningful
Pages and layouts are 2 places, there data can be fetched. WE don't fetch the data into blocks. Only in the containers, that are used as a component in Routes, so they are URL driven.
To fetch the data on the page we are using redial + react-router-redial
hook | beforeTransition | afterTransition | client | server |
---|---|---|---|---|
fetch |
+ | - | + | + |
defer |
- | + | + | - |
server |
- | + | - | + |
done |
- | + | + | + |
We're additional passing dispatch
and getState
methods to the hooks, so you can access the store and dispatch an action.
TODO
Cypress.io is used for E2E testing.
To run test, execute next command. You need server started and installed Chrome browser on your machine.
yarn test:acceptance
Tests are storing in cypress folder.
TODO. add jest to boilerplate
TODO. add argos CI to boilerplate
We are using gettext
standard for our basic localization process.
yarn locales:extract
This command will extract string for localization into locales/source.pot
file.
Use Poedit to create lang files based on .pot file.
Localization is configured in common/services/i18next.js
. Check the code to see how to import .po
file with new language to the app.
We are using Redux-form as a single way to work with the forms.
We use per-form validation. For validation we are using react-nebo15-validate
To add custom validation message, add it to ErrorMessages components. This component is used in Field components to show validation messages.
See the existing customization. Use this common in all places, when you need to display the error message based on error object.
Configuration of the 3rd party services is stored in common/services
. See the example of i18next and validations configurations. They are separately connected to client and server entrypoints.
See LICENSE.md.