Eslint Plugin React Hooks
React Hooks revolutionized how developers build function‑based components in React by introducing state and lifecycle features without classes. However, the flexibility of Hooks comes with a responsibility: using them correctly is essential for predictable behavior and bug‑free components. Enter eslint-plugin-react-hooks, the official ESLint plugin that enforces the Rules of Hooks and helps you maintain consistency in dependency arrays.
In this deep‑dive article, you’ll learn everything there is to know about eslint-plugin-react-hooks: why it exists, how to install and configure it, what rules it enforces, and advanced tips for real‑world projects. By the end, you’ll be confident in leveraging this tool to write more reliable, maintainable React applications.
What Is eslint-plugin-react-hooks?
eslint-plugin-react-hooks is an ESLint plugin maintained by the React team. Its primary function is to analyze your code and report violations of two critical Hooks rules:
- Rules of Hooks: Ensuring Hooks are only called at the top level of React function components or custom Hooks.
- Exhaustive Deps: Verifying the dependency arrays of useEffect, useCallback, and useMemo are complete and correct.
By integrating these checks into your development workflow, you catch mistakes early—during coding or continuous integration—rather than in subtle runtime bugs.
Why Enforce the Rules of Hooks?
React Hooks have two fundamental constraints:
- Call Order: Hooks must always be called in the same order on every render.
- Valid Context: Hooks can only be called from React function components or user‑defined custom Hooks.
Breaking these rules can lead to unpredictable state, skipped effects, or crashes. Consider the following invalid example:
function MyComponent({ condition }) {
if (condition) {
// ❌ Conditional Hook call: violates call order
useEffect(() => {
console.log('Effect ran');
}, []);
}
// ...
}ESLint’s rules-of-hooks rule automatically flags this pattern, guiding you to restructure code:
function MyComponent({ condition }) {
// ✅ Always call Hook at top level
useEffect(() => {
if (condition) {
console.log('Effect ran');
}
}, [condition]);
// ...
}Similarly, missing or incomplete dependencies in useEffect can cause stale values or unintended behavior. The exhaustive-deps rule ensures arrays include every referenced variable, avoiding those pitfalls.
Installation and Setup
Installing Dependencies
First, add the plugin (and React plugin, if you haven’t already) to your project:
npm install --save-dev eslint eslint-plugin-react eslint-plugin-react-hooks # or yarn add -D eslint eslint-plugin-react eslint-plugin-react-hooks
Ensure you already have eslint and eslint-plugin-react installed for basic linting and React‑specific checks.
Configuring ESLint
Next, update your ESLint configuration (e.g., .eslintrc.js, .eslintrc.json, or eslintConfig in package.json):
// .eslintrc.js
module.exports = {
parser: '@babel/eslint-parser', // or '@typescript-eslint/parser' for TS
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended' // <-- enables react-hooks rules
],
plugins: ['react', 'react-hooks'],
settings: {
react: {
version: 'detect',
},
},
rules: {
// other custom rules...
},
};The plugin:react-hooks/recommended preset automatically:
- Enables react-hooks/rules-of-hooks with severity error
- Enables react-hooks/exhaustive-deps with severity warn
You can override these in the rules section as needed.
Core Rules Explained
rules-of-hooks
- Rule Name: react-hooks/rules-of-hooks
- Purpose: Enforce that Hooks are called only at the top level of React functions.
- Error Example:
function Example({ count }) { if (count > 5) { // ❌ Invalid: Hook inside conditional useState(0); } return null; }- Correct Use:
function Example({ count }) { const [value, setValue] = useState(0); if (count > 5) { // logic here, but no additional Hook calls } return null; }
This rule also checks custom Hooks (functions whose names start with “use” and call other Hooks internally).
exhaustive-deps
- Rule Name: react-hooks/exhaustive-deps
- Purpose: Verify dependency arrays for Hooks such as useEffect, useCallback, and useMemo include all referenced values.
- Error Example:
function Timer({ duration }) { useEffect(() => { const id = setTimeout(() => { console.log(`Time's up: ${duration}`); }, duration); return () => clearTimeout(id); }, []); // 🚨 Missing 'duration' in deps return <div />; }- Correct Use:
function Timer({ duration }) { useEffect(() => { const id = setTimeout(() => { console.log(`Time's up: ${duration}`); }, duration); return () => clearTimeout(id); }, [duration]); return <div />; }
It also handles destructuring, nested property access, and function references, producing clear warnings when things go awry.
Advanced Configuration
Customizing Rule Severity
If you want to adjust severities:
rules: {
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': ['warn', {
additionalHooks: '(useMyCustomHook|useYourHook)'
}],
}- Set to "off" to disable
- Use ["warn", { ...options }] for warnings
- Use ["error", { ...options }] for errors
Ignoring Specific Patterns
To ignore certain Hooks or dependencies, pass regex patterns:
'exhaustive-deps': ['warn', {
// Treat calls matching this regex as "additional" Hooks
additionalHooks: '^useMy.*'
}]This is useful if you build your own Hooks that require similar dependency logic.
Combining with Other ESLint Plugins
Pair eslint-plugin-react-hooks with:
- eslint-plugin-jsx-a11y for accessibility
- eslint-plugin-import for import order
- eslint-plugin-prettier to integrate Prettier
Order your extends so formatting rules don’t conflict with React‑specific rules.
Working with TypeScript
For TypeScript projects, use @typescript-eslint/parser and install types:
npm install -D @typescript-eslint/parser @typescript-eslint/eslint-plugin
Example .eslintrc.js:
module.exports = {
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'react', 'react-hooks'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended'
],
settings: { react: { version: 'detect' } },
rules: {
// TypeScript-specific tweaks...
},
};The React Hooks plugin works seamlessly alongside TypeScript, ensuring type safety and correct Hook usage.
Integrating into CI/CD Pipelines
To enforce Hook rules in automated builds:
- Add an ESLint script in package.json:
{ "scripts": { "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}'" } }- Fail builds on lint errors:
# Example GitHub Actions job name: Lint on: [push, pull_request] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 18 - run: npm ci - run: npm run lint- Enforce fixes on pre-commit with Husky and lint-staged:
npm install -D husky lint-staged npx husky install
- In package.json:
{ "husky": { "hooks": { "pre-commit": "lint-staged" } }, "lint-staged": { "src/**/*.{js,jsx,ts,tsx}": ["eslint --fix", "git add"] } }
This ensures Hook rule violations are caught and remedied before code merges.
Common Pitfalls and How to Avoid Them
- Forgetting to Add New Variables to Deps
- Always heed exhaustive-deps warnings.
- Conditional Hook Calls
- Lift Hook calls out of conditions; guard inside callbacks.
- Complex Dependency Expressions
- Destructure variables and functions early to keep deps simple.
- Custom Hook Naming
- Prefix your own Hooks with use so the plugin recognizes them.
- Silent “Warnings” in Editors
- Configure your IDE (VS Code ESLint extension) to show warnings and errors inline.
Writing Custom Hook Lint Rules
Suppose you build a domain‑specific Hook, like useAuth. You might want to enforce patterns around it:
- Create a Custom ESLint Rule
- Use the AST explorer to detect calls to useAuth.
- Register in ESLint under a custom plugin namespace.
- Combine with additionalHooks in exhaustive-deps to treat it as a dependency‑aware Hook.
This advanced technique ensures your own abstractions benefit from the same lint guardrails as React core Hooks.
Real‑World Examples
Example: Data Fetching Hook
// src/hooks/useFetch.js
import { useState, useEffect, useCallback } from 'react';
export function useFetch(url) {
const [data, setData] = useState(null);
const fetchData = useCallback(async () => {
const resp = await fetch(url);
const json = await resp.json();
setData(json);
}, [url]);
useEffect(() => {
fetchData();
}, [fetchData]);
return data;
}ESLint will verify that both url (in useCallback) and fetchData (in useEffect) are correctly listed in dependency arrays.
Example: Feature Flag Hook
// src/hooks/useFeature.js
import { useContext, useMemo } from 'react';
import { FeatureContext } from '../context/FeatureContext';
export function useFeature(flag) {
const features = useContext(FeatureContext);
return useMemo(() => features[flag], [features, flag]);
}This pattern avoids unnecessary recomputations and ESLint confirms both features and flag appear in the useMemo deps.
Performance Considerations
- Over‑specifying Dependencies can cause too many effect runs. Aim for minimal yet complete arrays.
- Memoization: Use useCallback or useMemo sparingly—only when you’ve identified unnecessary re‑renders.
- Batching: React v18+ auto‑batches state updates, reducing some performance overhead.
Lint rules guide correctness; profiling tools (e.g., React DevTools Profiler) help you optimize further.
Migration and Adoption Strategies
- Start with Warnings: Configure exhaustive-deps as a warning to prevent blocking your team.
- Gradual Roll‑out: Introduce the plugin in a small codebase or feature branch.
- Documentation & Training: Educate teammates on Hook constraints and lint fixes.
- Automate Fixes: Use eslint --fix where possible to correct simple issues.
- Retrospectives: Review Hook violations in PRs to internalize best practices.
Conclusion
eslint-plugin-react-hooks is an indispensable tool for any React project using Hooks. By automating enforcement of the Rules of Hooks and dependency array correctness, it:
- Prevents subtle, hard‑to‑debug runtime errors
- Promotes consistent Hook usage across codebases
- Integrates seamlessly with ESLint, TypeScript, and modern build pipelines
Incorporate this plugin early in your development workflow to reap the benefits of reliable, maintainable React components. With proper configuration, CI/CD integration, and team education, you’ll unlock the full potential of Hooks while minimizing bugs and performance pitfalls.