ESLint has a comprehensive set of rules for JavaScript code that cover stylistic choices and prevent common bugs. Using ESLint alone will give your project a boost, but there are ESLint plugins available to add React-specific rules that will help you write solid React applications.
In this post, we’ll go over these ESLint rules and plugins, including as they apply to Hooks. Here are some quick links for you to jump around:
eslint-plugin-react-hooks
)
eslint-plugin-react
)
If you don’t have ESLint set up in your project yet, you can install it and generate an initial configuration by running the following command:
npm init @eslint/config
ESLint will ask some questions about your project. Among others, it asks if you are using React. Select this option, and ESLint will be installed along with eslint-plugin-react
.
If your application uses Create React App, you most likely won’t need to install any of the plugins mentioned in this article — or even ESLint at all. All versions of react-scripts (one of CRA’s packages) from v3 and later utilize an inbuilt Create React App ESLint configuration which is already set up to include the eslint-plugin-react-hooks
plugin (as well as the handy jsx-a11y
plugin).
N.B., manually installing ESLint, or other plugins mentioned in this article, in a CRA app may cause version conflicts or other issues
When using the React ESLint plugins, you should also add a React section to the ESLint configuration’s settings object. This lets you specify the version of React being used or use the latest
or detect
special values to use the latest react version or to detect which version of react the project is using, respectively:
{ ...other ESLint config settings: { react: { version: 'detect' } } }
There are some other less common configuration options available as well, as documented on the eslint-plugin-react
plugin’s npm page.
eslint-plugin-react-hooks
)This plugin only contains two rules, but they are critical to avoiding common pitfalls when writing function components with Hooks.
Install the package from npm:
npm install --save-dev eslint-plugin-react-hooks
To use the recommended configuration which activates both rules, you’ll need to update your ESLint configuration file, adding an entry to the extends
section for plugin:react-hooks/recommended
:
{ ... other ESLint config extends: [ 'eslint:recommended', 'plugin:react-hooks/recommended' ] }
If you find that rule violations are not being flagged, make sure you have included this in the extends
section.
This rule enforces that components follow the Rules of Hooks when using Hooks. The rules are discussed in detail in the React documentation, but there are two rules that must be followed when using Hooks:
In the default configuration, violations of this rule will cause an error, causing the lint check to fail.
This rule enforces certain rules about the contents of the dependency array that is passed to Hooks, such as useEffect
, useCallback
, and useMemo
. In general, any value referenced in the effect, callback, or memoized value calculation must be included in the dependency array. If this is not done properly, issues such as out-of-date state data or infinite rendering loops can result.
This rule is good at finding potential dependency-related bugs, but there are some limitations:
additionalHooks
, which expects a regular expression defining the names of your custom Hooks. The React team recommends against this in general, as discussed in the eslint-plugin-react-hooks
README. Instead, the team recommends providing a higher-level API.This rule has been somewhat controversial; there are several long issue threads on GitHub, but the React team has been good about soliciting and incorporating feedback. In the default configuration, violations of this rule are treated as warnings.
The details of this rule could take up an entire article on their own. For a deeper dive on this rule and how to properly use it, see the Understanding the React exhaustive-deps linting warning article, here on the LogRocket blog.
eslint-plugin-react
)This plugin contains a lot more rules (100 rules at time of writing) that are specific to the core of React. Most rules cover general React practices, and others cover issues related to JSX syntax. Let’s take a look at some of the more useful ones.
As discussed earlier, if your app is based on Create React App (and uses react-scripts), you already have this plugin installed. Otherwise, you can install it from npm:
npm install --save-dev eslint-plugin-react
To use the recommended rule set, just add 'plugin:react/recommended'
to your ESLint configuration’s extends
section:
{ ... other ESLint config extends: [ 'eslint:recommended', 'plugin:react/recommended', 'plugin:react-hooks/recommended' ] }
This recommended configuration includes a few rules that don’t apply if you are using React 17 or later. Recent versions of React include a new JSX transform that no longer requires React to be in scope in your components. This means you can remove imports such as import React from
'react';
when using JSX.
This will cause ESLint to report errors about React not being in scope (for more details, see the rule react/react-in-jsx-scope
below). If this applies to your application, you’ll need to add plugin:react/jsx-runtime
to your extends
list:
{ ... other ESLint config extends: [ 'eslint:recommended', 'plugin:react/recommended', 'plugin:react/jsx-runtime', 'plugin:react-hooks/recommended' ] }
This will disable the relevant rules and prevent ESLint from reporting them as errors.
Now that we have the plugin installed and configured, let’s take a look at some of the more useful ones.
For accessibility reasons, most clickable elements in a component that aren’t simple links to another URL should be implemented as buttons. A common mistake is to omit the type
attribute from these buttons when they aren’t being used to submit a form.
When no type
is specified, a button defaults to a type of submit
. This can cause issues for buttons that descend from a form
element. Clicking such a button inside of a form will cause a potentially unwanted form submission.
Action buttons that are not intended to submit a form should have a type
attribute of button
.
This rule enforces that all buttons explicitly have a type
attribute — even ones that are intended as Submit buttons. By being explicit, unintentional submissions are avoided and the intent of the code is clear.
Requires that all React components have their props described in a PropTypes
declaration. These checks only throw errors in development mode but can help catch bugs arising from the wrong props being passed to a component.
If your project uses TypeScript, this rule is also satisfied by adding a type annotation to the component props that describes them.
These two approaches are covered in detail in Comparing TypeScript and PropTypes in React applications by Dillion Megida.
Depending on the component, some props may be required while others are optional. If an optional prop is not passed to a component, it will be undefined
. This may be expected but can introduce bugs if the value is not checked.
This rule requires that every optional prop is given a default value inside of a defaultProps
declaration for the component. This default value can be explicitly set to null
or undefined
if that is what the component expects.
With function components, there are two different strategies that can be used to check default props:
This strategy expects the function component to have a defaultProps
object with the defaults.
const MyComponent = ({ action }) => { ... } MyComponent.propTypes = { Action: PropTypes.string; }; MyComponent.defaultProps = { action: 'init' };
This strategy expects the defaults to be specified in the function declaration, using JavaScript’s inbuilt default values syntax.
const MyComponent = ({ action = 'init' }) => { ... } MyComponent.propTypes = { Action: PropTypes.string; };
If you use the defaultArguments
strategy, there should not be a defaultProps
object. If there is, this rule will fail.
When rendering a list of items in React, we typically call map
on an array, and the mapping function returns a component. To keep track of each item in the list, React needs these components to have a key
prop.
A common pitfall with rendering lists is using the array index as the key. This can cause unnecessary or even incorrect renders. The React documentation advises against this practice due to the issues it can cause (there is also a more detailed discussion about how keys are used). A key is expected to be a unique identifier for that item, within the list, that does not change, like the primary key value in a database row.
This rule ensures that the array index is not used as the key.
Consider this simple React component:
const Greeter = ({ name }) => <div>Hello {name}!</div>;
The React
object is not referenced at all. However, React
still needs to be imported or else you will encounter an error. This is due to the transpilation process of JSX. Browsers don’t understand JSX, so during the build process (usually with a tool such as Babel or TypeScript), the JSX elements are transformed into valid JavaScript.
This generated JavaScript code calls React.createElement
in place of JSX elements. The above component might be transpiled to something like this:
const Greeter = ({ name }) => React.createElement("div", null, "Hello ", name, "!");
The references to React
here are why React
must still be imported. This rule ensures that all files with JSX markup (not necessarily even a React component) have React
in scope (typically through an import
or require
call).
Always importing React is necessary for proper transpilation, but when ESLint looks at the file, it’s still JSX, so it won’t see React
referenced anywhere. If the project is using the no-unused-vars
rule, this results in an error since React
is imported but not used anywhere.
This rule catches this situation and prevents no-unused-vars
from failing on the React
import.
For proper debugging output, all React components should have a display name. In many cases, this won’t require any extra code. If a component is a named function, the display name will be the name of the function. In the below examples, the display name of the component will be MyComponent
.
const MyComponent = () => { … }
const MyComponent = function() { return …; }
export default function MyComponent() { return …; }
There are some cases where the automatic display name is lost. This is typically when the component declaration is wrapped by another function or higher order component, like in the two examples below:
const MyComponent = React.memo(() => { … });
const MyComponent = React.forwardRef((props, ref) => { … });
The MyComponent
name is bound to the new “outer” component returned by memo
and forwardRef
. The component itself now has no display name, which will cause this rule to fail.
When these cases arise, a display name can be manually specified via the displayName
property to satisfy the rule:
const MyComponent = React.memo(() => { ... }); MyComponent.displayName = 'MyComponent';
React components accept a special prop called children
. The value of this prop will be whatever content is inside the opening and closing tags of the element. Consider this simple MyList
component:
const MyList = ({ children }) => { return <ul>{children}</ul>; };
This will render the outer ul
element, and any children we put inside the element will be rendered inside of it.
<MyList> <li>item1</li> <li>item2</li> </MyList>
This is the preferred pattern with React components. It is possible, though not recommended, to pass children as an explicit children prop:
<MyList children={<li>item1</li><li>item2</li>} />
The above usage will actually cause an error because JSX expressions, like the one passed as the explicit children prop, must have a single root element. This requires the children to be wrapped in a fragment:
<MyList children={<><li>item1</li><li>item2</li></>} />
As shown in the first example, children are passed as child elements to the component directly, so the component is the root element of the expression. No fragment or other enclosing element is needed here.
This is mainly a stylistic choice/pattern, but it does prevent inadvertently passing both an explicit children
prop and child elements:
<MyList children={<><li>item1</li><li>item2</li></>}> <li>item3</li> <li>item4</li> </MyList>
In this case, the child elements (item3
and item4
) would be displayed, but item1
and item2
would not. This rule ensures that children are only passed in the idiomatic way, as child JSX elements.
React’s dangerouslySetInnerHTML
prop allows arbitrary markup to be set as the innerHTML
property of an element. This is generally not recommended, as it can expose your application to a cross-site scripting (XSS) attack. However, if you know you can trust the input and the use case requires it, this approach may become necessary.
The prop expects an object with an __html
property, whose value is a raw HTML string. This string will be set as the innerHTML
.
Because this replaces any existing child content, it doesn’t make sense to use this in combination with a children
prop. In fact, React will throw an error if you attempt to do this. Unlike some errors that only appear in development mode (like PropTypes
validation errors), this error will crash your app.
This rule enforces the same rule. If dangerouslySetInnerHTML
is used with children, the lint rule will fail. It’s much better to catch these errors when linting, or at build time, rather than reported by users once the app is deployed!
Every time a React component is rendered, it comes at a performance cost. Oftentimes, certain patterns or practices can cause a component to unnecessarily re-render itself. There are many causes for this behavior, and this rule helps prevent one of them.
When any function is defined inside the component, it will be a new function object on every render. This means that whenever the component is re-rendered, the prop is considered changed. Even with React.memo
, the component will re-render.
If the child component has any useEffect
calls that take that function as a dependency, this can cause the effect to run again, creating the potential for an infinite loop that will likely freeze the browser.
With this rule enabled, any function that is passed as a prop will be flagged.
There are two ways this can be addressed. If the function does not depend on anything else inside the component, it can be moved outside of the component, where it is just a plain function that will always be the same memory reference. This ensures that the same function is passed to the prop each time.
For cases where the function does depend on the component in some way, the usual fix for this is to memoize it with the useCallback
Hook. Any properties referenced in the function will have to be included in the useCallback
dependency array; sometimes this requires multiple levels of memoization of values or functions.
This adds some complexity, but has the benefit of helping to reduce extra renders and prevent infinite loops.
The rules covered here are just a few of the ones provided by the eslint-plugin-react
plugin. Some rules can be opinionated or overzealous, but most also have configuration options to make them less strict.
There is also another very helpful ESLint plugin centered around JSX and accessibility practices: eslint-plugin-jsx-a11y
. The rules in this plugin check your JSX markup to make sure that good HTML accessibility practices are being followed.
These React ESLint plugins can be helpful to avoid common pitfalls, especially if you’re still new to React. You can even write your own rules and plugins to cover other situations!
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.