The Best ESLint Rules for React Projects
Tags: react, eslint, typescript, jsdoc
Published: 2023-09-16 03:56:47

Using ESLint for React projects can help catch some common mistakes, code-smells, and define common conventions for a codebase. In this blog, I'll go through some valuable ESLint plugins and rules tailored specifically for React projects.

Note that this blog is oriented towards usage of React in TypeScript with functional components. It also assumes you've already sorted your base ESLint/TypeScript/Prettier configs.

You can find my complete ESLint config on NPM: [`@tim-w-james/eslint-config`](https://www.npmjs.com/package/@tim-w-james/eslint-config)

## Official React Plugin

An obvious pick for React projects, but [`eslint-plugin-react`](https://github.com/jsx-eslint/eslint-plugin-react) along with their `plugin:react/recommended` rule set is a must. This will give you some sensible rules such as requiring a `key` to be specified in JSX arrays. [`eslint-config-airbnb`](https://github.com/airbnb/javascript/) is another good (if a bit loose) base rule set on top of `eslint-plugin-react` to start from.

I've tweaked the recommended rule set in a few ways:

```ts
"react/prefer-stateless-function": "error",
"react/button-has-type": "error",
"react/no-unused-prop-types": "error",
"react/jsx-pascal-case": "error",
"react/jsx-no-script-url": "error",
"react/no-children-prop": "error",
"react/no-danger": "error",
"react/no-danger-with-children": "error",
"react/no-unstable-nested-components": ["error", { allowAsProps: true }],
"react/jsx-fragments": "error",
"react/destructuring-assignment": [
  "error",
  "always",
  { destructureInSignature: "always" },
],
"react/jsx-no-leaked-render": ["error", { validStrategies: ["ternary"] }],
"react/jsx-max-depth": ["error", { max: 5 }],
"react/function-component-definition": [
  "warn",
  { namedComponents: "arrow-function" },
],
"react/jsx-key": [
  "error",
  {
    checkFragmentShorthand: true,
    checkKeyMustBeforeSpread: true,
    warnOnDuplicates: true,
  },
],
"react/jsx-no-useless-fragment": "warn",
"react/jsx-curly-brace-presence": "warn",
"react/no-typos": "warn",
"react/display-name": "warn",
"react/self-closing-comp": "warn",
"react/jsx-sort-props": "warn",
"react/react-in-jsx-scope": "off",
"react/jsx-one-expression-per-line": "off",
"react/prop-types": "off",
```

Some notable rules in the above config:

- [`react/no-unstable-nested-components`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-unstable-nested-components.md): components with components are an anti-pattern since they lose state when their parent is re-rendered (not to mention poor readability).
- [`react/destructuring-assignment`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/destructuring-assignment.md): this one is partially to enforce consistent code style, but also encourages you to make your state dependencies more efficient (rather than blindly passing in `props`).
- [`react/jsx-no-leaked-render`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-no-leaked-render.md): prefer conditional rendering via ternary expressions - an easy win to avoid unexpected values being rendered from `&&` or even crashes in rare cases.
- [`react/jsx-max-depth`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-max-depth.md): as soon as your components start reaching too far to the right, you know they're getting too big.
- [`react/function-component-definition`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/function-component-definition.md): standardise how you define your functional components.
- [`react/jsx-no-useless-fragment`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-no-useless-fragment.md) + [`react/jsx-fragments`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-fragments.md): more consistent JSX fragments.
- [`react/no-children-prop`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-children-prop.md): improves readability for wrapper components.
- [`react/no-unused-prop-types`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-unused-prop-types.md): flags any unused types. Note this has some limitations, as it won't flag props defined via the `React.FC<PropTypes>` generic.
- [`react/no-danger`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-danger.md): just in case a prop called `dangerouslySetInnerHTML` isn't enough to discourage it's use.
- [`react/display-name`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/display-name.md): no anonymous function components for better maintainability and integration with React's debugging tools.

Depending on your project, you might want to consider some of the following stricter rules:

- [`react/prefer-read-only-props`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/prefer-read-only-props.md): enforce immutability in your prop types.
- [`react/no-array-index-key`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-array-index-key.md): define better keys to avoid unnecessary re-renders.
- [`react/jsx-no-bind`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-no-bind.md): has performance benefits, preventing functions declared in a component from being created again on every re-render.
- [`react/jsx-props-no-spreading`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-props-no-spreading.md): helps with maintainability, readability and reduces the risk of invalid HTML props being passed to elements. However, I still believe spreading props has great utility for certain use-cases such as wrapper components, so use your own judgement on this one.
- [`react/no-multi-comp`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/no-multi-comp.md): one component per file. In my opinion, this isn't always ideal, especially for components with very specific uses that shouldn't be exposed to the wider codebase, but this can be mitigated somewhat by a good folder structure.

## Rules of Hooks

[`react-hooks`](https://www.npmjs.com/package/eslint-plugin-react-hooks) with the `plugin:react-hooks/recommended` rule set will save you more than a few headaches. Importantly, you can't call hooks conditionally, and will be warned if you state dependencies aren't exhaustive.

## React Refresh

[`react-refresh`](https://github.com/ArnaudBarre/eslint-plugin-react-refresh). Requires that `.tsx`/`.jsx` files only export components. Why? Because this optimises your app for [fash refresh](https://www.npmjs.com/package/react-refresh) to get a smoother development experience. If you're using [Vite](https://v2.vitejs.dev/guide/features.html#hot-module-replacement), you'll be utilising fash refresh under the hood and will want to enable this rule.

Turn it on with the config:

```ts
"react-refresh/only-export-components": "warn",
```

## JSX Ally

[`jsx-a11y`](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y) is all about ensuring your DOM elements are accessible. This plugin will prompt you to include the correct [ARIA attributes](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA) such as labels and roles, in addition to things like alt text.

The `jsx-a11y/recommended` ruleset has reasonable defaults, though ensure you map your [custom components to DOM elements](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y#component-mapping).

As an aside - as much as engineers like automated tooling, it will only get you so far in the world of accessibility. Using this plugin along with [Lighthouse](https://developer.chrome.com/docs/lighthouse/overview/) or additional A11y plugins for your E2E testing framework of choice is recommended, since that will help you identify additional issues like colour contrast (I'd suggest including these checks into your CI workflows). But ultimately, static accessibility analysis will help you identify basic mistakes, but isn't a substitute for some thorough manual testing for keyboard-only, screen reader, etc. usability (even better - get your target end-users involved in the process or seek an accessibility audit).

## Naming Conventions and Filename Rules

By convention, React components should be named in PascalCase. [`@typescript-eslint`](https://github.com/typescript-eslint/typescript-eslint) has the config we need, and though we can't specifically target React components, we can target `variable`s (and set some other conventions while we're at it):

```ts
"@typescript-eslint/naming-convention": [
  "warn",
  {
    selector: "default",
    format: ["camelCase"],
    leadingUnderscore: "allow",
  },
  {
    selector: "variable",
    // Specify PascalCase for React components
    format: ["PascalCase", "camelCase"],
    leadingUnderscore: "allow",
  },
  {
    selector: "parameter",
    format: ["camelCase"],
    leadingUnderscore: "allow",
  },
  {
    selector: "property",
    format: null,
    leadingUnderscore: "allow",
  },
  {
    selector: "typeLike",
    format: ["PascalCase"],
  },
],
```

Then we can enforce our file names to be `PascalCase` via [`filename-rules`](https://github.com/dolsem/eslint-plugin-filename-rules):

```ts
"filename-rules/match": [2, { ".ts": "camelcase", ".tsx": "pascalcase" }],
```

Finally, I'd also suggest requiring named exports via [`import`](https://github.com/import-js/eslint-plugin-import):

```ts
"import/no-default-export": "error",
```

## TS/JSDoc

We want to ensure our React components (and code more generally) is well documented. Using [`jsdoc`](https://www.npmjs.com/package/eslint-plugin-jsdoc) we can specify formatting requirements for our documentation, with [`tsdoc`](https://www.npmjs.com/package/eslint-plugin-tsdoc) for some TS specific syntax.

Extend `jsdoc/recommended-typescript` and specify some extra config:

```ts
"jsdoc/require-throws": "error",
"jsdoc/check-indentation": "warn",
"jsdoc/no-blank-blocks": "warn",
"jsdoc/require-asterisk-prefix": "warn",
"jsdoc/require-description": "warn",
"jsdoc/sort-tags": "warn",
"jsdoc/check-syntax": "warn",
"jsdoc/tag-lines": ["warn", "never", { startLines: 1 }],
"jsdoc/require-param": ["warn", { checkDestructuredRoots: false }],
"jsdoc/require-jsdoc": [
  "warn",
  {
    publicOnly: true,
    require: {
      FunctionDeclaration: true,
      FunctionExpression: true,
      ArrowFunctionExpression: true,
      ClassDeclaration: true,
      ClassExpression: true,
      MethodDefinition: true,
    },
    contexts: [
      "VariableDeclaration",
      "TSTypeAliasDeclaration",
      // Encourage documenting React prop types
      "TSPropertySignature",
    ],
    enableFixer: true,
  },
],
// tsdoc checks this syntax instead
"jsdoc/require-hyphen-before-param-description": "off",
"jsdoc/require-returns": "off",

"tsdoc/syntax": "warn",
```

This requires our exported components to be documented. Note that this has some limitations - in an ideal world we'd want prop types to be documented like so:

```tsx
type MyComponentProps = {
  /**
   * Some prop does something.
   */
  someProp: string;
};

/**
 * My component does a thing.
 */
export const MyComponent = ({ someProp }: MyComponentProps) => {
  ...
};
```

We extract our prop type definitions into their own type, then document each prop individually. This integrates with VSCode to display the description of `someProp` and is much cleaner than trying to document the destructured params.

However, the `jsdoc` ESLint plugin doesn't allow you to specify different requirements for different contexts depending on whether they are exported or not. We can use `TSPropertySignature`, but unless the prop types are exported, the rule won't be triggered. Feel free to reach out if you're aware of a solution to this limitation.

## Bonus - Arrow Function Styles

I prefer to set a standard for function declarations, so require use of arrow functions with an implicit return if possible. [`prefer-arrow-functions`](https://github.com/JamieMason/eslint-plugin-prefer-arrow-functions) can do this for us, noting we also need to override some default ESLint rules:

```ts
"prefer-arrow-functions/prefer-arrow-functions": [
  "warn",
  {
    classPropertiesAllowed: true,
    disallowPrototype: true,
    returnStyle: "unchanged",
  },
],
"arrow-body-style": "warn",
"prefer-arrow-callback": [
  "warn",
  {
    allowNamedFunctions: true,
  },
],
```

## Wrapping Up

Using the rules above will improve your code quality, consistency, and reduce the risk of bugs. However, every project is different and every team has their own styles and conventions, so I'd suggest tweaking rules to your needs. With that said, it's better to start with a stricter rule set then loosen it to reduce any false positives or noise. This encourages you to think critically about the code you write, and make the conscious decision to ignore rules if you need rather than be ignorant to certain anti-patterns.

My custom ESLint rule set applies the insights from this blog and can be installed via NPM: [`@tim-w-james/eslint-config`](https://www.npmjs.com/package/@tim-w-james/eslint-config). It also includes many useful ESLint and TypeScript rules that aren't specific to React.

Contributions and suggestions for extra rules are welcome.