Written by Joe Attardi✏️
Introduction
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:
React Hooks rules (eslint-plugin-react-hooks
)
This plugin only contains two rules, but they are critical to avoiding common pitfalls when writing function components with Hooks.
react-hooks/rules-of-hooks
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:
- Hooks should only be called from the top-level code of your component. What this really means is that the Hooks should not be called conditionally — they should instead be called on every render, in the same order, to avoid issues and subtle bugs
- Hooks should only be called from either a function component, or another Hook
- Custom Hooks often compose behavior together from inbuilt, or even other custom, Hooks
In the default configuration, violations of this rule will cause an error, causing the lint check to fail.
react-hooks/exhaustive-deps
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:
- Custom Hooks with dependency arrays will not be checked with this rule. It only applies to the inbuilt Hooks
- The rule can only properly check dependencies if it is a static array of values. If a reference to another array is used, or another array is spread into it, the rule will emit a warning that it cannot determine the dependencies
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.
React rules (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.
react/button-has-type
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.
react/prop-types
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.
react/require-default-props
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:
defaultProps
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'
};
defaultArguments
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.
react/no-array-index-key
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.
react/react-in-jsx-scope
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).
react/jsx-uses-react
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.
react/display-name
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/no-children-prop
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/no-danger-with-children
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!
react/jsx-no-bind
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.
Wrapping up
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!
Full visibility into production React apps
Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Top comments (0)