Boilerplate is a starter kit for quick development start, which allows you to save time on routine project setup and start developing right away.
The main advantage of our boilerplate is that there is nothing unnecessary in it.
Our boilerplate does not use Redux
, there are no unnecessary libraries that only complicate
life at the start of the project (such as Cypress
, or Typescript
).
We use the minimum number of dependencies necessary to concentrate on React
and not get distracted
on other things. But we also really like to keep our code clean and pretty, so our
boilerplate has everything you need to do that (ESLint
and Prettier
with strong configs).
We also really love the CSS-in-JS
approach and
we try to use it in all our React projects, so the boilerplate also uses styled-components
.
And, of course, we love writing unit tests and try to cover our applications with tests as much as possible. To do this
we mostly use Jest
and testing-library
.
- Just
React
and nothing extra - Clean code with
ESLint
+Prettier
- Quality tests with
Jest
+testing-library
- CSS-in-JS approach -
styled-components
- Modern components using hooks -
react-use
- Reliability of components using
prop-types
When creating a new project in React
it is very common to be faced with
routine work in the form of creating a project with CRA
,
installing a large number of third-party dependencies and setting them up to correctly
with each other.
Our boilerplate solves this problem very easily, it has everything for a quick start of the project.
You do not need to worry about the quality of code or the configuration of the testing environment. Our boilerplate
already use the latest tools to maintain code quality (ESLint
, Prettier
).
Also we have preconfigured Router
, a library for CSS-in-JS
and modern hooks React-use
.
You can just clone our boiler and start developing.
This boilerplate is designed mostly for junior developers, who find it difficult to set up a project on their own. Setting up a project in such a case can take quite a lot of time, which would be better spent on development or research the task. In order to relieve developers from the routine work of setting up the project and dependencies we decided to create this boilerplate with all the necessary tools to quickly setup and start development.
- English
- Russian
- React
- React-router-dom
- Styled-components
- React-use
- Prop-types
- Axios
- Jest
- Testing-library
- ESlint
- Prettier
- First start
- Project structure
- Routing
- Components
- LocalStorage
- Hooks
- Utils
- Axios
- Styles
- Testing
- Formatting
- JSDoc
- Running in production
- Cypress
- TypeScript
- Gitlab CI
- Tricks
This option is good in that you don't need to install a lot of dependencies on your working device. Docker just encapsulates all that trash.
To start the project with this option you need to install Docker and docker-compose
After, you just need to run the following command:
yarn docker:dev
When Docker
installs all the necessary dependencies and builds your application, you will see Compiled successfully
in your console. Your project is available on 3000
port; you can open it and start developing. - http://localhost:3000
If you can't or don't want to use docker
you can use the default method for starting your project using Node.JS and npm(yarn)
- Install dependencies
yarn # or npm i
- Start the project
yarn start # or npm start
The application is available on http://localhost:3000
.
├── README.md
├── package.json
├── yarn.lock
├── docker-compose.dev.yml
├── docker-compose.prod.yml
├── .env.example
├── .eslintrc
├── .gitlab-ci.yml
├── .stylelintrc
├── public
├── cli
├── docs
├── docker
│  ├── Dockerfile.dev
│  ├── Dockerfile.prod
├── src
│  ├── assets
│  │  ├── images
│  │  └── fonts
│  ├── components
│  │  └── [ComponentName]
│  │  │  └── index.js
│  │  │  └── [ComponentName].jsx
│  │  │  └── [ComponentName].test.jsx
│  │  │  └── [ComponentName].styles.js
│  ├── config
│  ├── pages
│  ├── hocs
│  ├── hooks
│  ├── index.js
│  ├── Router.jsx
│  ├── setupTests.js
│  ├── test.utils.js
│  ├── store
│  │  ├── configure.js
│  │  ├── index.js
│  │  ├── localStorage.js
│  │  ├── store.utils.js
│  │  ├── [storeName]
│  │  │  ├── actions.js
│  │  │  └── reducer.js
│  │  │  └── selectors.js
│  ├── utils
-
README.md - Project description
-
package.json - Npm configuration file
-
yarn.lock - Dependencies lockfile
-
docker-compose.dev.yml - docker-compose config for development
-
docker-compose.prod.yml - docker-compose config for build production
-
.env.example - environment examples file
-
.eslintrc - ESLint config
-
.gitlab-ci.yml - Gitlab CI config
-
.stylelintrc - Stylelint config
-
public - Folder with static files
-
cli - Helpful scripts(CLI)
-
docs - Additional documentation
-
docker - Dockerfiles' folder
-
docker/Dockerfile.dev - config for dev mode
-
docker/Dockerfile.prod - config for prod mode
-
src - Main development folder
-
src/assets - Static assets (images, fonts, etc.)
-
src/components - Components folder
-
src/components/[ComponentName] - A single component folder
-
src/components/[ComponentName]/index.jsx - Core file(logic, default export)
-
src/components/[ComponentName]/[ComponentName].jsx - View(markup)
-
src/components/[ComponentName]/[ComponentName].test.jsx - Unit-tests
-
src/components/[ComponentName]/[ComponentName].styles.js - Styles
-
src/config - Configuration files(axios, theme, etc.)
-
src/pages - Pages folder(routes)
-
src/hocs - Higher Order Components (HOCs)
-
src/hooks - React-hooks
-
src/index.js - Core application file(entry point)
-
src/Router.jsx - Main router
-
src/test.utils.js - Testing utils(testing-library)
-
src/utils - Utils folder(additional small reusable functions)
We use react-router-dom
for routing in the application.
All the routes are stored in the src/Router.jsx
file.
import React from 'react'
import { Route, Switch } from 'react-router-dom'
// pages
import { Main, Todo } from './pages'
function Router() {
return (
<Switch>
<Route exact path='/' component={Main} />
<Route exact path='/todo' component={Todo} />
</Switch>
)
}
export default Router
- Create the new file in the
src/pages
folder:
// NewPage.jsx
import React from 'react'
export default function NewPage() {
return (
<div>New page</div>
)
}
- Add the new page in
src/pages/index.js
file for better import:
import Main from './Main'
import Todo from './Todo'
import NewPage from './NewPage'
export {
Main,
Todo,
NewPage,
}
- Add the new page in
src/Router.jsx
file
import React from 'react'
import { Route, Switch } from 'react-router-dom'
// pages
import { Main, Todo, NewPage } from './pages'
function Router() {
return (
<Switch>
<Route exact path='/' component={Main} />
<Route exact path='/todo' component={Todo} />
<Route exact path='/new-page' component={NewPage} />
</Switch>
)
}
export default Router
- Documentation
- Important The pages are used only to logically separate different parts of the application. You don't need to use the pages as components. You can use
react-helmet
to set up the page's meta-tags (title, description, etc.)
When you work with the components, it's recommended to use a modern approach with functional components and hooks. It's not recommended to use class components because they work too slowly (performance) and won't be supported
To create the component, you can use the following CLI-script
:
yarn create:component MyComponent
After using this script, the folder with your component's name will appear in your src/components
folder. In this case, it will be the src/components/MyComponent
folder.
index.jsx - core file with logic. Example:
import React, { useState } from 'react'
import { useMount, useUpdateEffect } from 'react-use'
// view
import TodoList from './TodoList'
function Wrapper() {
const [todos, setTodos] = useState([])
const getInitialTodos = () => {
// ...some logic to get initialTodos from localStorage
}
const saveTodos = () => {
// ...some logic to save todos from localStorage
}
const addTodo = todo => setTodos([...todos, todo])
const removeTodo = todo => setTodos([...todos.filter(todo => todo === todo)]);
// Use mount-hook for calling getInitialTodos() after mount
useMount(() => getInitialTodos())
// Watch todos and save it in localStorage after updating
useUpdateEffect(() => saveTodos(), [todos])
// return view with some props
return <TodoList todos={todos} addTodo={addTodo} removeTodo={removeTodo} />
}
export default Wrapper
[ComponentName].jsx - view file (markup). Example:
import React from 'react'
import PropTypes from 'prop-types'
import TodoItem from '../TodoItem'
// prop-types
const propTypes = {
todos: PropTypes.arrayOf(PropTypes.object),
}
function TodoList({ todos }) {
if(!Boolean(todos.length)) return <div className="empty">No todos :)</div>
return (
<div className="todo-list">
{todos.map(todo => <TodoItem key={todo.id} todo={todo} />)}
</div>
)
}
TodoList.propTypes = propTypes
export default TodoList
[ComponentName].test.jsx - unit tests [ComponentName].styles.js - styles (styled-components by default)
To work with localStorage, you can use additional utilities: loadState
and saveState
Example:
import { saveState, loadState } from '../utils/localStorage'
const save = data => saveState(data, 'key')
const load = () => loadState('key')
This is a library of additional react hooks that meet most of the needs so that you do not have to reinvent the wheel every time. Hooks list Most useful hooks:
Creating custom hooks is a very useful thing as it allows reusing large amounts of code. If you see code that will probably be reused in the future, hook it.
This is an example of usaging a simple custom hook implementing work with API:
import { useState } from 'react'
import { useList, useToggle } from 'react-use'
import fetchImages from './fetchImages'
const useFetchImages = ({ source }) => {
const [images, imagesActions] = useList([])
const [isLoading, toggleLoading] = useToggle(false)
const [error, setError] = useState(null)
const fetchImages = async () => {
toggleLoading(true)
try {
const data = await fetchImages(source)
imagesActions.set(data)
} catch(err) {
setError(err)
}
toggleLoading(false)
}
return {
images,
isLoading,
error,
fetchImages,
}
}
export default useFetchImages
Utilities are stored in the src/utils
folder in separate files.
camelToSnakeCase
andsnakeToCamelCase
- transformation of a string into various styles of writing phrases without spaces or punctuationnormalizeObjectKeys
- transformation of all the object fields (keys) usingsnakeToCamelCase
normalizeCollectionKeys
- transformation of all the elements (Element should be an object) of the array usingnormalizeObjectKeys
getRequestParams
- function for getting values of get-parameters from location.search.localStorage
- utilities for working with localStorage
PS: The list will be updated
In working with API requests, the most useful library is axios
with async/await
syntax.
axios
configuration is in the src/config/api.js
file.
If you need to add a header in the existing axios instance, you can use setApiHeader
function. Example:
import { setApiHeader, api } from '../config'
async function authenticate() {
// Authorization
const response = await api.post('/auth')
// Getting token from response
const { token } = response
// Set header for the next authenticated requests
setApiHeader('Authorization', `Bearer ${token}`)
}
authenticate()
Note: Always try to use async/await syntax
To work with environment variables, we need to use some config files:
.env.example
- for storing examples of variables.env
- for variables
To add a new environment variable, you need to do the following steps:
- Add variables into
.env.example
file with empty value
REACT_APP_API_BASE_URL=
- Add variable with value into
.env
file.
REACT_APP_API_BASE_URL=https://google.com/api
- Restart the project (required)
- Add the variable into the config (
src/config/index.js
)
export const config = {
API_URL: process.env.REACT_APP_API_BASE_URL,
}
- Use variable from config
axios.get(config.API_URL)
Note: Don't forget to restart the project after adding/updating any variables
Note 2: Environment variables should be ALWAYS started by REACT_APP_
; otherwise, they won't work
To write styles, we can use several approaches:
- Scss/BEM - Default styling
- Css-In-JS (styled-components) - a recommended option that is simpler and more convenient than the previous one.
In testing the components (unit testing), Jest
and React-testing-library
are used.
Jest Testing-library React-testing-library
There are several scripts to run tests:
yarn test
- watch-mode
yarn test:coverage
- watch-mode+coverage
yarn test:ci
- without watch-mode + coverage + disable coloring output
Coverage generates after running yarn test:coverage
command.
You can see expanded coverage in the HTML format in the ./coverage
folder.
Note: Unit tests also have a minimal coverage threshold. If coverage is less than 80%, the tests will fail
Linters are to keep code clean. They prevent shitcode from getting into a repository.
Eslint is used for linting Javascript code. Airbnb config is used as default.
To run a linter, you can use the following npm-scripts:
yarn lint:js
- to run a linter
yarn lint:js:fix
- to run a linter with autofix
To lint css code, stylelint is used. The linter checks your code for typos and spelling mistakes.
To run the linter, you can use yarn lint:css
script
Note: To run both linters, you can use yarn lint:all
script
The optimal solution to make your code more readable and cleaner is to use JSDoc. The project doen't use JSDoc by default, but you can easily add it using the following helpful links: Documentation eslint-plugin
To run the project in production, you can use yarn docker:prod
script. This script does the following steps:
- Download dependencies
- Build the project(
yarn build
) - Run nginx to serve static content
Cypress is a framework for end-to-end testing based on Javascript.
You can have 100% code coverage with unit tests, which test all your components separately, but your application can still fail when the components start to interact with each other. To prevent possible fails, you need to use e2e tests with Cypress. Cypress can test everything that works in a browser.
1. Check system requirements
yarn add cypress --dev
To run Cypress Test Runner
, you need to execute the following command:
yarn run cypress open
1. Setup CI
Configuration for GitLab CI:
.gitlab-ci.yml:
# first, install Cypress, then run all tests (in parallel)
stages:
- build
- test
# to cache both npm modules and Cypress binary we use environment variables
# to point at the folders we can list as paths in "cache" job settings
variables:
npm_config_cache: "$CI_PROJECT_DIR/.npm"
CYPRESS_CACHE_FOLDER: "$CI_PROJECT_DIR/cache/Cypress"
# cache using branch name
# https://gitlab.com/help/ci/caching/index.md
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .npm
- cache/Cypress
- node_modules
# this job installs NPM dependencies and Cypress
install:
image: cypress/base:10
stage: build
script:
- npm ci
# check Cypress binary path and cached versions
# useful to make sure we are not carrying around old versions
- npx cypress cache path
- npx cypress cache list
- $(npm bin)/print-env CI
- npm run cy:verify
- npm run cy:info
# all jobs that actually run tests can use the same definition
.job_template:
image: cypress/base:10
stage: test
script:
# print CI environment variables for reference
- $(npm bin)/print-env CI
# start the server in the background
- npm run start:ci &
# run Cypress test in load balancing mode
- npm run e2e:record -- --parallel --group "electrons on GitLab CI"
artifacts:
when: always
paths:
- cypress/videos/**/*.mp4
- cypress/screenshots/**/*.png
expire_in: 1 day
# actual job definitions
# all steps are the same, they come from the template above
electrons-1:
extends: .job_template
electrons-2:
extends: .job_template
electrons-3:
extends: .job_template
electrons-4:
extends: .job_template
electrons-5:
extends: .job_template
Full documetation of using Cypress with CI you can find here.
To run Cypress in your CI, you just need to do the following steps:
2.1. Install
yarn add cypress --dev
2.2. Run
cypress run
- Setup your project
- Add
--record
flag when you are runningcypress run
in your CI.cypress run --record --key = abc123
Useful links:
- https://www.valentinog.com/blog/cypress/
- https://blog.logrocket.com/how-to-write-useful-end-to-end-tests-with-cypress/
- https://softchris.github.io/pages/cypress.html#great-e2e-testing-with-cypress
yarn add typescript @types/node @types/react @types/react-dom @types/jest
You need to create the configuration file tsconfig.json
. You can create it using the following command:
npx tsc --init
{
"compilerOptions": {
"noImplicitAny": false, /* Raise error on expressions and declarations with an implied any type */
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"lib": ["dom", "dom.iterable", "esnext"], /* List of library files to be included in the compilation */
"allowJs": true, /* Allow javascript files to be compiled. */
"skipLibCheck": true, /* Skip type checking of all declaration files (*.d.ts) */
"esModuleInterop": true, /* Emit __importStar and __importDefault helpers for runtime babel ecosystem compatibility and enable --allowSyntheticDefaultImports for typesystem compatibility */
"allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking */
"strict": true, /* Enable all strict type checking options.
Enabling --strict enables --noImplicitAny, --noImplicitThis, --alwaysStrict, --strictBindCallApply, --strictNullChecks, --strictFunctionTypes and --strictPropertyInitialization */
"forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
"module": "esnext", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"moduleResolution": "node", /* Determine how modules get resolved. Either "Node" for Node.js/io.js style resolution, or "Classic" */
"resolveJsonModule": true, /* Include modules imported with .json extension. */
"isolatedModules": true, /* Perform additional checks to ensure that separate compilation (such as with transpileModule or @babel/plugin-transform-typescript) would be safe. */
"noEmit": true, /* Do not emit outputs. */
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement /*
"jsx": "react", /* Support JSX in .tsx files: "react", "preserve", "react-native" */
"baseUrl": "./src" /* Base directory to resolve non-relative module names */
},
"include": ["src"]
}
If you need additional information about setting up the tsconfig file, you can check compiler options
Then, you need to change the extensions of all your .js(x)
files to .ts(x)
.
If you need to disable type checking for any line, you need to add the following comment before the line:
// @ts-ignore
If you need to disable type checking for all files, you need to add the following comment at the beginning of the file:
// @ts-nocheck
Cheatsheets
Applications examples
- Create React App TypeScript Todo Example 2020
- Ben Awad's 14 hour Fullstack React/GraphQL/TypeScript Tutorial
- Cypress Realworld App
- install - installing dependencies using
yarn
- lint - code linting
yarn lint:all
- test - running unit tests using
yarn test:ci
script; building and displaying coverage - pages - building the project
yarn build
- pages:deploy - deploy to gitlab-pages
To create the component, you can use the following CLI-script
:
yarn create:component MyComponent
After using this script, the folder with your component's name will appear in your src/components
folder. In this case, it will be the src/components/MyComponent
folder.
Here is a list of available snippets to quickly create some entities:
mdocmp
- componentmdstyle
- styled-components filemdcompunit
- unit-tests for componentmdpage
- pagemdhook
- custom hook
These snippets are automatically available in your VSCode
because they are set up for project. You can see and edit any snippet in the .vscode/madboiler-snippets.code-snippets
file
vscode-styled-components
- styled-components supportVisual Studion IntelliCode
- intelliSense for VSCode(AI-assit)TODO Highlight
- highlight your #todosReact PropTypes Intellisense
- intelliSense for PropTypesPrettier
- for autoformattingPath Intellisense
- intelliSense for importsESLint
- lint highlight