The Best ESLint Rules for React Projects

timwjames

TimJ

Posted on September 16, 2023

The Best ESLint Rules for React Projects

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

Official React Plugin

An obvious pick for React projects, but 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 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:

"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",
Enter fullscreen mode Exit fullscreen mode

Some notable rules in the above config:

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

  • react/prefer-read-only-props: enforce immutability in your prop types.
  • react/no-array-index-key: define better keys to avoid unnecessary re-renders.
  • react/jsx-no-bind: has performance benefits, preventing functions declared in a component from being created again on every re-render.
  • react/jsx-props-no-spreading: 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: 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 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. Requires that .tsx/.jsx files only export components. Why? Because this optimises your app for fash refresh to get a smoother development experience. If you're using Vite, you'll be utilising fash refresh under the hood and will want to enable this rule.

Turn it on with the config:

"react-refresh/only-export-components": "warn",
Enter fullscreen mode Exit fullscreen mode

JSX Ally

jsx-a11y is all about ensuring your DOM elements are accessible. This plugin will prompt you to include the correct ARIA attributes 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.

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 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 has the config we need, and though we can't specifically target React components, we can target variables (and set some other conventions while we're at it):

"@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"],
  },
],
Enter fullscreen mode Exit fullscreen mode

Then we can enforce our file names to be PascalCase via filename-rules:

"filename-rules/match": [2, { ".ts": "camelcase", ".tsx": "pascalcase" }],
Enter fullscreen mode Exit fullscreen mode

Finally, I'd also suggest requiring named exports via import:

"import/no-default-export": "error",
Enter fullscreen mode Exit fullscreen mode

TS/JSDoc

We want to ensure our React components (and code more generally) is well documented. Using jsdoc we can specify formatting requirements for our documentation, with tsdoc for some TS specific syntax.

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

"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",
Enter fullscreen mode Exit fullscreen mode

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:

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

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

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 can do this for us, noting we also need to override some default ESLint rules:

"prefer-arrow-functions/prefer-arrow-functions": [
  "warn",
  {
    classPropertiesAllowed: true,
    disallowPrototype: true,
    returnStyle: "unchanged",
  },
],
"arrow-body-style": "warn",
"prefer-arrow-callback": [
  "warn",
  {
    allowNamedFunctions: true,
  },
],
Enter fullscreen mode Exit fullscreen mode

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. It also includes many useful ESLint and TypeScript rules that aren't specific to React.

Contributions and suggestions for extra rules are welcome.

💖 💪 🙅 🚩
timwjames
TimJ

Posted on September 16, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related