- Project Structure
- Contributing
- Tests
- Code linting
- Files organization & file naming convention
- Component & Container separation pattern
- Others
- Conventions
.
├── nginx/ # Nginx server configuration for production
├── public/ # Static files
├── src/ # Single page application source script
├── .dockerignore # .dockerignore
├── .eslintrc # Eslint configuration
├── .gitignore # .gitignore
├── .travis.yml # CI/CD script
├── CONTRIBUTING.md # Contributing guidelines
├── docker-compose.yml # Docker compose script to set dev environment
├── Dockerfile # Docker file
├── package.json # package.json
├── README.md # README
└── yarn.lock # yarn.lock
- docker>=17.0.0
- docker-compose>=1.17.0
- node>=9.0.0
- yarn>=1.6.0
#. If not yet done, clone project locally
git clone <project-url> && cd <project-folder>
#. If not yet done, install node dependencies
yarn install # install node dependencies locally
#. Start application
docker-compose up # start application in dev mode
You can now access application in dev mode at http://localhost
- Create a new branch
Requirements
- New branch MUST be started from master (which is our dev branch)
- Feature branch MUST be named
feature/[a-zA-Z0-9\-]+
- Bug fix branch MUST be named
fix/[a-zA-Z0-9\-]+
- Branch refering to an open issue MUST be named
(fix|feature)/<issue-ID>
-
Develop on new branch being careful to write test for every change you proceed to
-
Push branch
git push origin -u <branch>
Pushing will trigger a CI pipeline (see section CI/CD)
- Create a pull request
yarn add <package> # install package, and update package.json & yarn.lock
Command above can sometime error with EACCES
code, this means that docker wrote some files (usually cache) in your local node_modules
folder.
To solve it you can change right access of the folder by running command
sudo chown -R $USER:$USER node_modules
-
Create a release branch
release/x.x.x
-
Bump to version
x.x.x
package.json
: change version tox.x.x
CHANGES.md
: ensure release sectionx.x.x
documentation is exhaustive and set release date
-
Commit with message
bump version to x.x.x
git commit -am "bump version to x.x.x"
- Tag version
git tag -a x.x.x -m "Version x.x.x"
-
Bump to version
increment(x.x.x)-dev
package.json
: change version toincrement(x.x.x)-dev
CHANGES.md
: create emptyincrement(x.x.x)-dev
section with unreleased status
-
Push branch and tags
git push origin -u release/x.x.x --follow-tags
- Proceed to merge request
release/x.x.x
->master
Run test
yarn test
Run coverage
yarn test -- --coverage
Test framework
We use Jest test framework (directly packed into create-react-app
We use Enzyme Shallow rendering to test component
Requirements
- Tests MUST be written in a
**/__tests__
folder located in the same directory as the element tested - Tests file for
<filename>.js
MUST be named<filename>.test.js
- SHOULD use Jest snapshot testing
Folder structure
src/
:
├── path/to/folder/
│ ├── __test__/
│ │ ├── file1.test.js
│ │ └── file2.test.js
│ ├── file1.js
│ └── file2.js
:
TODO: complete this section
Framework
We use ESLint (packed in create-react-app)
This project use a combination of husky, lint-staged, prettier to format code automatically including .js,.jsx, .json and .css (c.f create-react-app documentation.
Some pre-commit hooks are set-up so whenever you make a commit, prettier will format the changed files automatically.
Requirements
- Code MUST respect linting rules defined in
.eslintrc
We make active usage of redux, if not familiar with redux we recommend going through redux documentation before going through this section.
Requirements
- Actions MUST go into a into src/redux/actions
- Reducers MUST go into a into src/redux/reducers
- Enhancers MUST go into a into src/redux/enhancers
Folder structure
src/
:
├── redux/
│ ├── actions/ # Actions
│ ├── enhancers/ # Enhancers
│ └── reducers/ # Reducers
:
We highly recommend you reading more about structuring reducer
Requirements
- State structure (or state shape) MUST be defined in terms of domain data & app status, it MUST NOT be defined after your UI component tree
- Root reducer (feeded to createStore()) MUST combine together specialized reducers:
- data reducers, handling the data application needs to show, use, or modify (typically information retrieved from some APIs)
- status reducers, handling information related to application's behavior (such as "there is a request in progress")
- ui reducer, handling how the UI is currently displayed (such as "Sidebar is open")
- Global state shape MUST reflect src/redux/reducers/ folder structure. Keys in global state MUST be the same as file names src/redux/reducers/
- Specialized reducer files MUST implement a reducer function exported as default
Folder structure
src/
:
├── redux/
│ :
│ ├── reducers/
│ │ ├── data/
│ │ │ ├── domain1.js # Reducer responsible to handle state slice related to domain 1 data
│ │ │ └── domain2.js # Reducer responsible to handle state slice related to domain 2 data
│ │ ├── status/
│ │ │ └── bahavior1.js # Reducer responsible to handle state slice related to application status behavior 1
│ │ └── ui/
│ │ └── element1.js # Reducer responsible to handle state slice related to UI element 1
: :
Corresponding state shape would be
{
data : {
domain1: {},
domain2: {}
},
status : {
category1: {}
},
ui : {
element1: {}
}
}
Example
// Import action of interest as constants
import { OPEN_SIDEBAR, CLOSE_SIDEBAR } from "../actions/ui";
// Define initial state
const initialState = {
open: false
};
// Implement "reducer" function with initial state as default state
cexport default (state = initialState, { type }) => {
switch (type) {
case OPEN_SIDEBAR:
return {
...state,
open: true
};
case CLOSE_SIDEBAR:
return {
...state,
open: false
};
default:
return state;
}
};
Actions are payloads of information that send data from your application to your store. They are the only source of information for the store. You send them to the store using store.dispatch().
Requirements
- Actions MUST be grouped by piece of state they cover (e.g. UI actions are defined into src/redux/actions/ui.js)
- Each action type MUST be declared as a constant
- Each action SHOULD come with an action creator function
- Action types & creator functions MUST be implemented in the same file
- Actions MUST be serializable (if you think in passing a function into an action for handling by reducers, it means that you actually need a redux middleware)
- Actions MUST follow [Flux-Standard-Action] (FSA)(https://github.com/redux-utilities/flux-standard-action#actions) format
- MUST be a plain JavaScript object
- MUST have a
type
property - MAY have an
error
property - MAY have an
payload
property - MAY have an
meta
property - MUST NOT include property other than
type
,error
,payload
andmeta
- There SHOULD NOT be a 1-to-1 link between actions and reducers. Typically an action could be reduced by multiple reducers
Folder structure
src/
:
├── redux/
│ ├── actions/ # Component file goes into a folder named after the component
│ │ ├── ui.js # Action related to UI modification
│ │ └── status.js # Actions related to status information
:
Example
// Declare action type as a constant
export const ADD_TODO = "ADD_TODO";
// Declare action creator
export const addTodo = (text, category) => ({
// Respect FSA standard format (type, payload, meta properties)
type: ADD_TODO,
payload: {
text
},
meta: {
category
},
});
Store enhancers are higher-order function that composes a store creator to return a new, enhanced store creator. In our case, we mainly use enhancer to add redux middlewares allowing to hook custom behaviors when dispatching actions.
We highly recommend you reading more about middlewares in redux
Requirements
- Middlewares MUST go into src/redux/enhancers/middlewares
Folder structure
src/
:
├── enhancers/
│ ├── middlewares/ # Middlewares
│ │ ├── index.js # Combine middlewares into one enhancer
│ │ └── router.js # Middleware managing router
│ ├── reduxDevTools.js # Enhancer to include ReduxDevTool
│ └── index.js # Combine enhancers into on root enhancer
:
Requirements
- All components MUST go into src/components
- Components MUST have a unique name
Requirements
- Component files MUST go into a into a folder named after the component in src/components/
- Component code MUST be split into as many atomic sub components as necessary
- Component MUST follow naming pattern path-based-component-naming, which consists in naming the component accordingly to its relative path from src/components/
Examples of those components are
- Skeleton elements such as AppBar, Sidebar
- View pannels such as Home
- Layout elements such as Layout
Folder structure and file naming
src/
:
├── components/
│ :
│ ├── AppBar/ # Component file goes into a folder named after the component
│ │ ├── __tests__/ # Component tests folder
│ │ ├── AppBar.js # Main component file (named as the folder)
│ │ ├── IconButton.js # Sub component file (we do not repeat AppBar in file's name)
│ │ ├── Title.js # Sub component file (we do not repeat AppBar in file's name)
│ │ └── Toolbar.js # Sub component file (we do not repeat AppBar in file's name)
: : :
Component naming
Main component
# src/components/AppBar/AppBar.js
// Follow path-based-component-naming (not repeating)
const AppBar = () => (
// Component code comes here
);
Sub component
# src/components/AppBar/IconButton.js
// Follow path-based-component-naming
const AppBarIconButton = () => (
// Component code comes here
);
Requirements
- Generic UI components MUST NOT held business logic specific to the application (they actually could be stored on some external npm library)
- MUST follow naming pattern path-based-component-naming, which consists in naming the component accordingly to its relative path from src/components/UI
Examples of those components are: Button, Input, Checkbox, Select, Modal, etc…
Folder structure and file naming
src/
:
├── components/
│ :
│ ├── UI/
│ │ ├── ListItem/
│ │ │ ├── __test__/
│ │ │ ├── 1.js
│ │ │ └── 2.js
│ │ ├── Button/
│ │ │ ├── __test__/
│ │ │ └── 1.js
: : :
Requirements
- Container MUST go into src/containers
- Container MUST follow same relative path from src/containers as the component it wraps from src/components
- Container MUST have same name as the component it wraps
Folder structure
src/
:
├── containers/
│ :
│ ├── AppBar/
│ │ ├── __tests__/
│ │ ├── AppBar.js
│ │ ├── IconButton.js
│ │ └── Title.js
: : :
We respect a separation between presentational components & containers.
- Better separation of concerns makes app understandable and better UI writing components this way.
- Better reusability. Same presentational component can be used with completely different state sources, and turn those into separate container components that can be further reused.
- Presentational components are essentially app’s “palette”. It is possible to put them on a single page and let designers tweak all their variations without touching the app’s logic. It is possible to run screenshot regression tests on that page.
- This forces to extract “layout components” such as Sidebar, AppBar, etc. and use this.props.children instead of duplicating the same markup and layout in several container components.
You can read more about it here
Requirements
- SHOULD implement some DOM markup and styles of their own
- MAY contain both presentational components and containers
- SHOULD allow containment via this.props.children.
- SHOULD NOT have their own state (when they do, it MUST be UI state and MUST NOT be data)
- SHOULD be written as functional components unless they need state, lifecycle hooks, or performance optimizations
- MUST receive data and callbacks exclusively via props.
- MUST NOT have dependencies with the rest of the app, such as redux actions or stores
- MUST NOT specify how the data is loaded or mutated (API calls MUST NOT be defined in a component)
- MUST NOT define any route
Requirements
- MAY import components from UI libraries typically Material-UI
- MAY import components and containers from the rest of the application
- SHOULD NOT import any resources related to Redux, except compose() that is sometime convenient to connect multiple marterial-ui wrappers (withStyles(), withTheme()...)
- SHOULD NOT have any dependencies in the rest of the application, except components or containers
- MUST be organized in 3 ordered sections: 1. low-level React imports / 2. Material-UI imports / 3. intra-application imports
Example
// Section 1: React low level imports
import React from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import { compose } from "redux";
// Section 2: Material-UI imports
import MuiAppBar from "@material-ui/core/AppBar";
import { withStyles } from "@material-ui/core/styles";
// Section 3: Components & Containers import from the application
import AppBarToolbar from "./Toolbar";
Requirements
- MUST be a function taking theme as argument and returning an object
Example
const styles = theme => ({
appBar: {
position: "fixed",
top: 0,
zIndex: theme.zIndex.drawer + 1,
transition: theme.transitions.create(["width", "margin"], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
})
},
appBarShifted: {
marginLeft: 240,
width: `calc(100% - ${240}px)`,
transition: theme.transitions.create(["width", "margin"], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen
}),
[theme.breakpoints.down("sm")]: {
width: "100%"
}
}
});
Requirements
- SHOULD be a function taking a props object as an argument (except lifecycle, UI state or some optimization are required)
- MUST respect compenent naming convention (see below)
- MUST be atomic
- MUST be agnostic of the rest of the application, this MUST include every variable namings. Think it as it should be able to exist on its own
- MUST be documented with PropTypes
Example
const AppBar = ({
classes,
shifted, // for agnosticity we use variable name "shifted" over "sidebarOpen"
}) => (
<MuiAppBar
position="absolute"
className={classNames(
classes.appBar,
shifted && classes.appBarShifted
)}
>
<AppBarToolbar shifted={shifted} />
</MuiAppBar>
);
// Documentation with PropTypes
AppBar.propTypes = {
classes: PropTypes.object.isRequired,
shifted: PropTypes.bool
};
Requirements
- MAY inject styles information using withStyle(), withTheme(), withWidth()
- There MUST be one unique export
Example
Basic: only withStyle()
export default withStyles(styles)(AppBar);
Advanced: using compose() from redux
export default compose(
withTheme(),
withStyles(styles)
)(AppBar);
Requirements
- MAY contain both presentational components and containers
- SHOULD NOT define DOM markup of their own (except for some wrapping divs)
- MUST NOT have styles
- MAY provide the data and behavior to presentational or other container components.
- MAY organise components/containers using routes
- MAY implement redux related elements mapStateToProp(), mapDispatchToProps(), mergeProps() and connect it to presentational component using connect()
- MAY implement API calls or other side effects
- MAY be stateful, as they tend to serve as data sources
Requirements
- MAY import elements from state management libraries elements (redux, react-redux)
- MAY import elements from src/redux such as actions or selectors
- MAY import elements from routing library
- MAY import Material-UI utilities such as isWidthUp()
Example
// Section 1: React/Reduxd low level imports
import React, { Component } from "react";
import { connect } from "react-redux";
// Section 2: internal imports
import AppbarButton from "../../components/AppBar/Button";
import LayoutSkeleton from "../../components/Layout/Skeleton";
import { openSidebar } from "../../redux/actions/ui";
import { HOME } from "../../constants/routes";
Requirements
- SHOULD NOT define DOM markup of their own (except for some wrapping divs)
- MAY define route
- MAY implement hooks on React lifecycle
- MUST respect
Example
Lifecycle container
class WithLifecycleHooks extends Component {
componentDidMount() {
// Could perform some API calls here
}
render() {
// return a presentational component
}
}
Routing container
const Layout = () => (
<Switch>
<Route path="/" exact component={() => <Redirect to={HOME} />} />
<Route component={LayoutSkeleton} />
</Switch>
);
Implement mapStateToProps(state, [ownProps]), mapDispatchToProps(dispatch, [ownProps]), mergeProps(stateProps, dispatchProps, ownProps)
Requirements
- MAY implement mapStateToProps(state, [ownProps])
- MAY implement mapDispatchToProps(dispatch, [ownProps]). If it is only required to map action creators it MUST be an object
- MAY implement mergeProps(stateProps, dispatchProps, ownProps)
- Aggregating props MUST NOT be performed within container, ownProps and mergeProps() allow to accomplish it properly out of the component (c.f. https://github.com/reduxjs/react-redux/blob/master/docs/api.md)
Example
const mapStateToProps = state => ({
shifted: state.ui.sideBarOpen
});
const mapDispatchToProps = {
onClick: openSidebar
};
Requirements
- MAY connect redux information to a comp
- MUST be one unique export
Example
export default connect(mapStateToProps)(AppBar);
Material UI theme is declared in ./src/constants/theme.js
To apply styling on specific components and not in the full app, use Material-UI overrides
To make UI fully responsive, you can use Material-UI breakpoints
- Use Lighthouse of Google in order to audit your PWA and see what's missing (icons, https, ...)
- Follow the guidelines here (Google opinionated way of building PWA)
-
MUST This word, or the terms "REQUIRED" or "SHALL", mean that the definition is an absolute requirement of the specification.
-
MUST NOT This phrase, or the phrase "SHALL NOT", mean that the definition is an absolute prohibition of the specification.
-
SHOULD This word, or the adjective "RECOMMENDED", mean that there may exist valid reasons in particular circumstances to ignore a particular item, but the full implications must be understood and carefully weighed before choosing a different course.
-
SHOULD NOT This phrase, or the phrase "NOT RECOMMENDED" mean that there may exist valid reasons in particular circumstances when the particular behavior is acceptable or even useful, but the full implications should be understood and the case carefully weighed before implementing any behavior described with this label.
-
MAY This word, or the adjective "OPTIONAL", mean that an item is truly optional. One vendor may choose to include the item because a particular marketplace requires it or because the vendor feels that it enhances the product while another vendor may omit the same item. An implementation which does not include a particular option MUST be prepared to interoperate with another implementation which does include the option, though perhaps with reduced functionality. In the same vein an implementation which does include a particular option MUST be prepared to interoperate with another implementation which does not include the option (except, of course, for the feature the option provides.)