How to add a custom ESLint configuration to a Create React App project
Background
Every front-end project should have some sort of static code analyzing tool. This will ensure that your team sticks to one coding style and avoids known anti-patterns in development.
Arguably, one of the best lint tools for JavaScript projects is ESLint (opens in a new tab). It supports a variety of plugins to extend the functionality and has rich east-to-use documentation. ESLint can also be configured to work with TypeScript projects hence previously dominated TSLint (opens in a new tab) was deprecated in favor of ESLint.
In this post, we will look at ESLint integration on both JavaScript and TypeScript based React Projects created with Create React App (opens in a new tab) (CRA) boilerplate.
Do I need a custom ESLint configuration?
Probably not. Because Create React App comes with ESLint already integrated. They use their own sharable ESLint configuration (opens in a new tab) and this can be found under the eslintConfig object in package.json
.
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
If you are fine with using the configuration provided in the boilerplate, you can skip reading now 🙃.
To checkout the rules and plugins used in react-app ESLint config, click here (opens in a new tab).
Why use a custom configuration?
Mind you that most of the ESLint rules are tailored for a specific individual or team. For example, using single quotes over double quotes will depend on preference.
It is always better to define your own lining rules based on your/team’s preference if you are working on a long-term project.
Pre-requisites
- NodeJS (opens in a new tab) & npm (opens in a new tab).
- An app created with Create React App (opens in a new tab) boilerplate.
- ESLint plugin configured in the IDE/Editor. (VSCode Plugin (opens in a new tab) | WebStorm Plugin (opens in a new tab))
Let’s get started
Remove the existing config
Go to package.json
at the root of the project, and remove the eslintConfig object**.**
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
Add ESLint configuration
Inside the root directory, let's create a .eslintrc.js
file. There are other formats (opens in a new tab) too but I personally prefer the JS format.
# from the root directory
touch .eslintrc.js
Let’s start with the following basic configuration.
module.exports = {
env: {
browser: true, // Browser global variables like `window` etc.
commonjs: true, // CommonJS global variables and CommonJS scoping.Allows require, exports and module.
es6: true, // Enable all ECMAScript 6 features except for modules.
jest: true, // Jest global variables like `it` etc.
node: true // Defines things like process.env when generating through node
},
extends: [],
parser: "babel-eslint", // Uses babel-eslint transforms.
parserOptions: {
ecmaFeatures: {
jsx: true
},
ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
sourceType: "module" // Allows for the use of imports
},
plugins: [],
root: true, // For configuration cascading.
rules: {},
settings: {
react: {
version: "detect" // Detect react version
}
}
};
This will basically define the environments and parser options.
Now we’ll improve the configuration by adding some useful sharable configurations and plugins.
Add Sharable Configurations (Presets)
✅ eslint:recommended
Enables few key rules in ESLint rule book (opens in a new tab).
✅ plugin:react/recommended
Enables the recommended (opens in a new tab) React rule set in eslint-plugin-react (opens in a new tab).
✅ plugin:jsx-a11y/recommended
Enables the recommended accessibility rules in eslint-plugin-jsx-a11y (opens in a new tab).
✅ plugin:react-hooks/recommended
Enables React Hooks best practices rule set in eslint-plugin-react-hooks (opens in a new tab).
✅ plugin:jest/recommended
Enables recommended rules in eslint-plugin-jest (opens in a new tab)
✅ plugin:testing-library/react
Enables recommended settings in eslint-plugin-testing-library (opens in a new tab)
extends: [
"eslint:recommended",
"plugin:react/recommended",
"plugin:jsx-a11y/recommended",
"plugin:react-hooks/recommended",
"plugin:jest/recommended",
"plugin:testing-library/react"
],
Add Plugins
✅ eslint-plugin-import
This plugin intends to support the linting of ES2015+ (ES6+) import/export syntax, and prevent issues with the misspelling of file paths and import names.
plugins: [
"import" // eslint-plugin-import plugin. https://www.npmjs.com/package/eslint-plugin-import
],
Add Rules
You can override the rules defined in the presets to your own liking. I like to have 4 space indentations, double quotes, etc. I can now specify that in the rules object like below.
rules: {
indent: [
"error",
4
],
quotes: [
"warn",
"double"
]
}
Also, I will define the sort order of the imports. This rule is supplied by the eslint-plugin-import
plugin we added in the previous step.
"import/order": [
"warn",
{
alphabetize: {
caseInsensitive: true,
order: "asc"
},
groups: [
"builtin",
"external",
"index",
"sibling",
"parent",
"internal"
]
}
]
You can also use plugin:import/recommended as a preset but i like to define my own sorting method. Check the docs (opens in a new tab) for more info.
Optional: If you use lodash (opens in a new tab) in your project and if your build system supports tree shaking, you can restrict the use of the CommonJS imports and non-tree-shakable modules using the following rule.
"no-restricted-imports": [
"error",
{
paths: [
{
message: "Please use import foo from 'lodash-es/foo' instead.",
name: "lodash"
},
{
message: "Avoid using chain since it is non tree-shakable. Try out flow instead.",
name: "lodash-es/chain"
},
{
importNames: ["chain"],
message: "Avoid using chain since it is non tree-shakable. Try out flow instead.",
name: "lodash-es"
},
{
message: "Please use import foo from 'lodash-es/foo' instead.",
name: "lodash-es"
}
],
patterns: [
"lodash/**",
"lodash/fp/**"
]
}
],
Following is the final configuration 🎉. I have enabled few more rules as per my liking and feel free to modify them based on your requirements.
module.exports = {
env: {
browser: true, // Browser global variables like `window` etc.
commonjs: true, // CommonJS global variables and CommonJS scoping.Allows require, exports and module.
es6: true, // Enable all ECMAScript 6 features except for modules.
jest: true, // Jest global variables like `it` etc.
node: true // Defines things like process.env when generating through node
},
extends: [
"eslint:recommended",
"plugin:react/recommended",
"plugin:jsx-a11y/recommended",
"plugin:react-hooks/recommended",
"plugin:jest/recommended",
"plugin:testing-library/react"
],
parser: "babel-eslint", // Uses babel-eslint transforms.
parserOptions: {
ecmaFeatures: {
jsx: true
},
ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
sourceType: "module" // Allows for the use of imports
},
plugins: [
"import" // eslint-plugin-import plugin. https://www.npmjs.com/package/eslint-plugin-import
],
root: true, // For configuration cascading.
rules: {
"comma-dangle": [
"warn",
"never"
],
"eol-last": "error",
"import/order": [
"warn",
{
alphabetize: {
caseInsensitive: true,
order: "asc"
},
groups: [
"builtin",
"external",
"index",
"sibling",
"parent",
"internal"
]
}
],
indent: [
"error",
4
],
"jsx-quotes": [
"warn",
"prefer-double"
],
"max-len": [
"warn",
{
code: 120
}
],
"no-console": "warn",
"no-duplicate-imports": "warn",
"no-restricted-imports": [
"error",
{
paths: [
{
message: "Please use import foo from 'lodash-es/foo' instead.",
name: "lodash"
},
{
message: "Avoid using chain since it is non tree-shakable. Try out flow instead.",
name: "lodash-es/chain"
},
{
importNames: ["chain"],
message: "Avoid using chain since it is non tree-shakable. Try out flow instead.",
name: "lodash-es"
},
{
message: "Please use import foo from 'lodash-es/foo' instead.",
name: "lodash-es"
}
],
patterns: [
"lodash/**",
"lodash/fp/**"
]
}
],
"no-unused-vars": "warn",
"object-curly-spacing": [
"warn",
"always"
],
quotes: [
"warn",
"double"
],
"react/jsx-curly-spacing": [
"warn",
{
allowMultiline: true,
children: {
when: "always"
},
spacing: {
objectLiterals: "always"
},
when: "always"
}
],
"react/jsx-filename-extension": [
"error",
{
extensions: [
".js",
".jsx",
".ts",
".tsx"
]
}
],
"react/jsx-indent": [
"error",
4,
{
checkAttributes: true,
indentLogicalExpressions:
true
}
],
"react/jsx-indent-props": [
"error",
4
],
"react/prop-types": "warn",
semi: "warn",
"sort-imports": [
"warn",
{
ignoreCase: false,
ignoreDeclarationSort: true,
ignoreMemberSort: false
}
],
"sort-keys": [
"warn",
"asc",
{
caseSensitive: true,
minKeys: 2,
natural: false
}
]
},
settings: {
react: {
version: "detect" // Detect react version
}
}
};
Configuration for TypeScript Projects
If you created a TypeScript project using the CRA TypeScript template, use the overrides (opens in a new tab) object in the configuration to apply the rules to TypeScript files.
overrides: [
{
files: [ "**/*.ts?(x)" ],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaFeatures: {
jsx: true
},
ecmaVersion: 2018,
sourceType: "module"
},
plugins: [
"@typescript-eslint"
],
// You can add Typescript specific rules here.
// If you are adding the typescript variant of a rule which is there in the javascript
// ruleset, disable the JS one.
rules: {
"@typescript-eslint/no-array-constructor": "warn",
"no-array-constructor": "off"
}
}
],
You only need to add rules to this section if the base ESLint rule is not supporting TypeScript of you want to add a certain rule only to TypeScript files. Most rules work for both TypeScript & JavaScript.
Add ESLint ignore file
Create a .eslintignore
file to ignore certain files/folders from linting. You can ignore the node_modules, distribution folders, cache folders etc.
# from the root directory
touch .eslintignore
node_modules
public
Add helper npm Scripts
CRA will usually show the Lint warnings/errors in the terminal when you run the application.
Also if you have the ESLint plugins properly configured in you Editor or IDE, the errors/warnings will be shown inline.
But it is always best to create npm scripts so that you can use them in CI systems as well.
For JavaScript projects, use the following npm scripts.
"scripts": {
"lint": "eslint -c .eslintrc.js --ext .js,.jsx .",
"lint:fix": "npm run lint -- --fix"
}
For TypeScript projects, use the following npm scripts.
"scripts": {
"lint": "eslint -c .eslintrc.js --ext .js,.jsx,.ts,.tsx .",
"lint:fix": "npm run lint -- --fix"
}
Run the Scripts
The following command will run the linter for the project and report if there are any issues.
npm run lint
The following script will autofix (opens in a new tab) the possible errors.
npm run lint:fix
Now you have a working application with ESLint configurations. If you need, check out the following optional steps to further configure your setup.
Optional Steps
As an additional step, I like to make sure that any code that violates our ESLint config doesn’t get pushed to the codebase. So basically, i need to enforce running ESLint before a Git commit.
We can easily accomplish the requirement using husky (opens in a new tab) and lint-staged (opens in a new tab).
What is Husky?
Husky can be used to run scripts before certain Git Hooks are executed. Read the docs (opens in a new tab).
What is Lint Staged?
Runs linters against staged git files.
Setting Up
- Install Husky
npx husky-init && npm install
2. Install lint-staged.
npm install --save-dev lint-staged
3. Create a lint-staged configuration file.
touch lint-staged.config.js
There are many ways you can add the configuration file. I prefer the JS config. Check out the documentation (opens in a new tab) for alternatives.
4. Add the lint-staged configuration.
module.exports = {
"*.+(js|jsx)": [
"npm run lint"
]
};
For TypeScript projects, add ts
and tsx
as well as the blob pattern**.**
“*.+(js|jsx|ts|tsx)”
5. Add an npm script to run lint staged.
Add the following script under the script section in package.json
.
"lint:staged": "lint-staged",
Without this, husky will complain about lint-staged command being missing. I guess you can use npx to run lint staged, but this method is cleaner IMO 😉.
6. Add a pre-commit hook.
npx husky add .husky/pre-commit "npm run lint:staged"
Testing the flow
I intentionally made a lint violation in App.js
and tried to commit a file.
I got the bellow error as expected and I wasn’t allowed to commit to the repository.
Conclusion
Hope you found this blog post useful. Feel free to try this out and if you have any suggestions regarding the blog you can log an issue in this repo (opens in a new tab).
Links
- JavaScript Config (opens in a new tab)
- TypeScript Config (opens in a new tab)
- JavaScript Demo App Source Code (opens in a new tab)
- TypeScript Demo App Source Code (opens in a new tab)
Signing off… ✌️❤️