how-to-add-a-custom-eslint-configuration-to-a-create-react-app-project

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

  1. NodeJS (opens in a new tab) & npm (opens in a new tab).
  2. An app created with Create React App (opens in a new tab) boilerplate.
  3. 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

ESLint erros on the terminal

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

  1. 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.

Husky & Lint Staged in Action

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

Signing off… ✌️❤️