The First Step to Clean Code - using a linter

Ayushman SachanAyushman Sachan
7 min read

Have you ever come across code that looked like this?

And wished that you hadn’t decided to become a Software Developer? Well, that’s what happens when you work on a project where no one bothers to setup basic development tools like a linter. So let’s take the first steps, and setup ESLint on a modern React project, bootstrapped with Vite. My editor for choice for this tutorial will be Visual Studio Code.

Okay, I lied. The first step to write better code — is to use TypeScript. It implements strict type checking, which enforces a developer to write code that is more maintainable and ultimately less error prone. However, type checking alone can't accomplish much when it comes to code readability. What we need for that, is a comprehensive and opinionated set of formatting rules that we can check our code against. Thankfully, ESLint has been around for some time now, and it is quite configurable when it comes to all kinds of linting rules.


Setting up dependencies

Using the @eslint/config utility package

The ESLint team provides a very handy initialisation package that you can use for quickly setting up a modern ESLint config with necessary dependencies — @eslint/config. To use it, run

npm init @eslint/config@latest

It will ask a few questions about your project, and then install the necessary packages according to your requirements. Using this utility will also create an eslint.config.js file for you. These are the packages it auto-installs for a TypeScript + React project.

  • eslint  —  the base package, required for enabling linting

  • @eslint/js — utility package for JavaScript specific functionality of ESLint

  • eslint-plugin-react —  ESLint plugin for React rules

  • typescript-eslint — TypeScript support for ESLint

  • globals — Package for identifying different JavaScript environments and their globals

Once the script finishes execution, your eslint.config.js should be looking something like this —

import pluginJs from "@eslint/js";
import pluginReact from "eslint-plugin-react";
import globals from "globals";
import tseslint from "typescript-eslint";

export default [
  { files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"] },
  { languageOptions: { globals: globals.browser } },
  pluginJs.configs.recommended,
  ...tseslint.configs.recommended,
  pluginReact.configs.flat.recommended,
];

Manual Setup

You can also manually install the same packages, and create an eslint.config.js yourself. Copy the contents from the above snippet into the file.

If you are upgrading your ESLint to v9.0.0 or above, read more about migrating from the older typescript-eslint setup here.

npm install --save-dev eslint@^9.0.0 @eslint/js eslint-plugin-react globals 

# for use with TypeScript
npm install --save-dev typescript-eslint

Configuring ESLint

With these packages installed, we can now move on to configuring ESLint. If you have created a project with Vite, chances are that it created an ESLint config for you, named .eslintrc.cjs, which you can safely remove, in favour of the new "flat config". You can read more about it in this ESLint blog post.

If your project does not specify "type":"module" in its package.json file, then eslint.config.js must be in CommonJS format.

What are configs and plugins?
Now, ESLint configurations can get pretty complicated if you try to do it from scratch. That's why shareable configs and plugins exist — they make it much easier to build a config without jumping through the hoops of understanding each and every rule of ESLint. Shareable configs are a set of ESLint rules, that you can use directly in your config by "extending" them. Plugins, on the other hand, add more rules to the ESLint ecosystem, often specific to a library — like React. For a detailed rundown, see the shareable config and plugin docs.

Before moving on to adding some rules to the config, let's run the linter with the recommended rules that it has provided for us. Make sure your lint script in package.json does not have the --ext flag in it, since that has been deprecated in the newer version of ESLint.

npm run lint

This will run ESLint on your source code, and output a bunch of warnings and/or errors. Wait, do you see too many of these errors?

 error  'React' must be in scope when using JSX

In modern React, we do not need to import React to use JSX in our code. So why is ESLint complaining? It's because the "recommended" config of eslint-plugin-react flags it as an error. Instead, we want to extend the jsx-runtime config. Change the line to use jsx-runtime like so —

// imports

export default [
  { files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"] },
  { languageOptions: { globals: globals.browser } },
  pluginJs.configs.recommended,
  ...tseslint.configs.recommended,
  pluginReact.configs.flat['jsx-runtime'],
];

Ignore files

You might notice that ESLint is also linting files from the dist or coverage folder right now, and we don't want that. In your eslint.config.js file, if an ignores key is used without any other keys in the configuration object, then the patterns act as global ignores. It looks something like this —

// imports

export default [
  {
    ignores: [
      'coverage',
      'dist',
      '.eslintrc.cjs',
      'vite.config.ts',
      'vitest.config.ts',
    ],
  },
  // other configs
];

Adding rules

You can completely customise our config by adding or removing rules, even adjusting their severity. You can also insert rules from plugins, for example, if we don't want to use the recommended configurations that they provide (this requires specifying the plugin under the plugins key). To configure a rule that is defined within a plugin, prefix the rule ID with the plugin namespace and /

// imports

export default [
  // ignores
  // other configs
  {
    plugins: {
      '@typescript-eslint': tseslint.plugin,
    },
    rules: {
      // this rule is made available from the @typescript-eslint plugin
      '@typescript-eslint/naming-convention': [
        'error',
        {
          selector: ['parameter', 'variable'],
          leadingUnderscore: 'require',
          format: ['camelCase'],
          modifiers: ['unused'],
        },
        {
          selector: ['parameter', 'variable'],
          leadingUnderscore: 'allowDouble',
          format: ['camelCase', 'PascalCase', 'UPPER_CASE'],
        },
      ],
    }
  }
];

ESLint Auto Fix

ESLint provides an auto-fix feature that can automatically fix some issues regarding formatting of your code. You can use it by enabling the --fix flag in your package.json lint script —

// ...
// other scripts
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
"lint:fix": "eslint . --report-unused-disable-directives --max-warnings 0 --fix",
// ...

Setup ESLint VSCode extension

For a better developer experience, we will also be installing the official ESLint extension for VSCode. Once it is installed, it will look for eslint.config.js in your project and provide real time linting errors and warnings in your editor. To use the auto-fix feature in the GUI, we'll enable it in VSCode settings. Add these lines to your VSCode user or workspace (project specific) settings —

  "editor.codeActionsOnSave": {
    "source.addMissingImports.ts": "explicit",
    "source.organizeImports": "explicit",
    "source.fixAll.eslint": "explicit"
  },
  "editor.formatOnSave": true,
  "eslint.format.enable": true,
  "eslint.useFlatConfig": true,

ESLint should now be available as a formatter for supported filetypes, and will automatically lint and fix some problems when you save the file.


Using older eslintrc configs in flat configs

For backwards compatibility, ESLint provides the FlatCompat utility through the @eslint/eslintrc package. We'll be using a popular eslintrc config from the Airbnb team for this example that is used in many React projects. However, you may want to use a different config, or build your own — it depends on how large of a project you're working on, and ultimately personal preference.

npm install @eslint/eslintrc --save-dev

Next, we'll setup the compat object by mimicking CommonJS global variables. Add these lines just above your config export —

// other imports
import { FlatCompat } from "@eslint/eslintrc";
import path from "path";
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const compat = new FlatCompat({
  baseDirectory: __dirname, // optional; default: process.cwd()
  resolvePluginsRelativeTo: __dirname, // optional
});

// export default [...];

We can now use the airbnb config —

// ...

export default [
  // global ignores
  ...compat.extends("airbnb"),
  // ...
];

Conclusion

In conclusion, setting up a linter is not a choice for any modern project, it's a must. You should always strive to write clean code and tools like ESLint are used by the biggest and smallest projects all around for a reason.
However, remember that there's no such thing as clean code, but there's definitely ugly code, and you know it when you see it.

12
Subscribe to my newsletter

Read articles from Ayushman Sachan directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ayushman Sachan
Ayushman Sachan

I'm a software engineer from India with keen interest in building complex software systems. I specialise in building data-driven web user experiences. I like to write about tools and technologies that interest me.