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",
Some notable rules in the above config:
-
react/no-unstable-nested-components
: 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
: this one is partially to enforce consistent code style, but also encourages you to make your state dependencies more efficient (rather than blindly passing inprops
). -
react/jsx-no-leaked-render
: 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
: as soon as your components start reaching too far to the right, you know they're getting too big. -
react/function-component-definition
: standardise how you define your functional components. -
react/jsx-no-useless-fragment
+react/jsx-fragments
: more consistent JSX fragments. -
react/no-children-prop
: improves readability for wrapper components. -
react/no-unused-prop-types
: flags any unused types. Note this has some limitations, as it won't flag props defined via theReact.FC<PropTypes>
generic. -
react/no-danger
: just in case a prop calleddangerouslySetInnerHTML
isn't enough to discourage it's use. -
react/display-name
: 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
: 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",
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 variable
s (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"],
},
],
Then we can enforce our file names to be PascalCase
via filename-rules
:
"filename-rules/match": [2, { ".ts": "camelcase", ".tsx": "pascalcase" }],
Finally, I'd also suggest requiring named exports via import
:
"import/no-default-export": "error",
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",
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) => {
...
};
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,
},
],
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.
Top comments (7)
Thank you! It really helps!
I also applied the rules in github.com/dooboolab-community/esl...
The only difference is that I prefer
function-declaration
in react/function-component-definition.Also, I found a bug in react/no-unused-prop-types.
You can see the
false
negative in above screenshot 🤔It looks like false negatives for
react/no-unused-prop-types
is a known issue: github.com/jsx-eslint/eslint-plugi.... Given that it doesn't work withReact.FC
types anyway, I'm considering disabling this in my rule set, though it is useful at times.There is Unicorn plug-in, which also enforces great best practices.
When you are writing code, you have a conversation with at least 3 people. The machine, you, and yourself in the future.
If you are working with a team, you are looking at many more people.
It is clear what readable code is for the machine. What readable means for humans is defined by the rules which we use to write prose.
Novelist, bloggers, news writers or even poets are the authority on that, not some mindless algorithm.
Thank you! 🙏
Awesome config, thanks!
Thanks 👌