A lightweight, proactive development tool that helps you catch React performance issues before they become problems. Unlike traditional profiling tools that require you to detect issues first, React Render Checkup proactively flags potential re-render problems during development through both static analysis (ESLint) and runtime detection (React hooks).
React performance issues often stem from unnecessary re-renders caused by:
- Inline arrow functions and object literals passed as props
- Unstable dependencies in hooks
- Missing or incorrect dependency arrays
- Props that change references unnecessarily
These issues are usually discovered late in development or in production when performance problems surface.
React Render Checkup provides two complementary tools:
- ESLint Plugin - Catches issues during development through static analysis
- React Hook - Provides runtime tracking with visual "cause trees" to trace render issues to their source
This monorepo contains two packages:
eslint-plugin-react-render-checkup- ESLint rules for static analysisreact-hook-checkup- Runtime hooks for render tracking
npm install --save-dev eslint-plugin-react-render-checkupAdd to your .eslintrc.js:
module.exports = {
plugins: ["react-render-checkup"],
extends: ["plugin:react-render-checkup/recommended"],
// Or configure individual rules:
rules: {
"react-render-checkup/no-inline-function-props": "warn",
"react-render-checkup/exhaustive-deps": "error",
"react-render-checkup/require-stable-deps": "warn",
},
};npm install --save-dev react-hook-checkupUse in your components:
import { useRenderCheckup } from "react-hook-checkup";
function MyComponent({ data, onUpdate }) {
useRenderCheckup("MyComponent", { data, onUpdate });
// Your component logic
return <div>...</div>;
}Statically analyze code for common performance footguns:
// β Bad - Creates new function on every render
<Button onClick={() => handleClick()} />;
// β
Good - Stable reference
const handleButtonClick = useCallback(() => handleClick(), []);
<Button onClick={handleButtonClick} />;ESLint Rules:
no-inline-function-props- Prevents inline arrow functions, objects, and arrays as propsexhaustive-deps- Enhanced dependency checking for React hooksrequire-stable-deps- Suggests memoization for unstable dependencies
Ensures dependencies are stable and exhaustive:
// β Warning: unstable dependency
function MyComponent() {
const config = { api: "/api/data" }; // New object every render
useEffect(() => {
fetchData(config);
}, [config]); // This will cause infinite re-renders!
}
// β
Good
function MyComponent() {
const config = useMemo(() => ({ api: "/api/data" }), []);
useEffect(() => {
fetchData(config);
}, [config]);
}Instead of just listing changed props, get a visual tree showing the root cause:
import { useRenderCheckup } from "react-hook-checkup";
function ChildComponent({ data, onUpdate }) {
useRenderCheckup(
"ChildComponent",
{ data, onUpdate },
{
logToConsole: true,
trackCauseTree: true,
}
);
return <div>{data.name}</div>;
}Console output:
π ChildComponent rendered (#3)
Changed props: onUpdate
π‘ Suggestions:
- useCallback: Function prop "onUpdate" creates new reference on each render
Cause tree: {
componentName: "ChildComponent",
propName: "onUpdate",
reason: "Unstable function passed as prop",
parent: null
}
Get actionable suggestions on where to optimize:
// Component with performance issues
function ParentComponent() {
const [count, setCount] = useState(0);
// This causes ChildComponent to re-render unnecessarily
const config = { theme: "dark" };
return <ChildComponent config={config} />;
}React Render Checkup will suggest:
π‘ Suggestion: Object prop "config" creates new reference on each render
Consider using useMemo:
const config = useMemo(() => ({ theme: 'dark' }), []);
Tracks component renders and detects performance issues.
Parameters:
componentName(string) - Name of the componentprops(object) - Current props to trackoptions(object) - Configuration options
Options:
interface CheckupOptions {
enabled?: boolean; // Enable/disable tracking (default: true in dev)
logToConsole?: boolean; // Log to console (default: true)
trackCauseTree?: boolean; // Build cause tree (default: true)
includeValues?: boolean; // Include prop values in logs (default: false)
onRender?: (info: RenderInfo) => void; // Custom callback
}HOC to automatically track renders:
const TrackedComponent = withRenderCheckup(MyComponent, {
logToConsole: true,
});Get statistics for a component:
const stats = getRenderStats("MyComponent");
// {
// totalRenders: 10,
// unnecessaryRenders: 3,
// averageChangedProps: 1.5
// }getTrackedComponents()- Get list of all tracked componentsclearCheckupData()- Clear all tracking dataexportCheckupData()- Export data as JSON
Disallows inline functions, objects, and arrays as props.
Options:
{
"react-render-checkup/no-inline-function-props": ["warn", {
"allowedProps": ["style", "className", "key"],
"checkAllComponents": false
}]
}Ensures hook dependencies are complete and necessary.
{
"react-render-checkup/exhaustive-deps": "error"
}Suggests memoization for unstable dependencies.
{
"react-render-checkup/require-stable-deps": "warn"
}import React, { useState, useCallback, useMemo } from "react";
import { useRenderCheckup } from "react-hook-checkup";
function TodoList({ initialTodos }) {
useRenderCheckup("TodoList", { initialTodos });
const [todos, setTodos] = useState(initialTodos);
const [filter, setFilter] = useState("all");
// β
Memoized to prevent unnecessary re-renders
const filteredTodos = useMemo(() => {
return todos.filter((todo) => {
if (filter === "completed") return todo.completed;
if (filter === "active") return !todo.completed;
return true;
});
}, [todos, filter]);
// β
Stable callback reference
const handleToggle = useCallback((id) => {
setTodos((prev) =>
prev.map((todo) =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
}, []);
return (
<div>
<FilterButtons onFilterChange={setFilter} />
{filteredTodos.map((todo) => (
<TodoItem key={todo.id} todo={todo} onToggle={handleToggle} />
))}
</div>
);
}
function TodoItem({ todo, onToggle }) {
useRenderCheckup("TodoItem", { todo, onToggle });
return <div onClick={() => onToggle(todo.id)}>{todo.text}</div>;
}π TodoList rendered (#2)
Changed props: initialTodos
β Component rendered due to legitimate prop change
π TodoItem rendered (#5)
β οΈ Unnecessary render - no props changed
π‘ Suggestions:
- React.memo: Component re-rendered without prop changes
π TodoItem rendered (#3)
Changed props: onToggle
π‘ Suggestions:
- useCallback: Function prop "onToggle" creates new reference on each render
Cause tree: {
componentName: "TodoItem",
propName: "onToggle",
reason: "Unstable function passed as prop"
}
import { useRenderCheckup } from "react-hook-checkup";
function MyComponent(props) {
useRenderCheckup("MyComponent", props, {
onRender: (info) => {
// Send to analytics
if (info.changedProps.length === 0) {
trackEvent("unnecessary_render", {
component: info.componentName,
renderCount: info.renderCount,
});
}
},
});
return <div>...</div>;
}import { exportCheckupData } from "react-hook-checkup";
// Export after testing session
const data = exportCheckupData();
console.log(data); // Full render history as JSONBoth packages are designed for development use only. The React hook automatically disables in production (NODE_ENV === 'production').
Contributions are welcome! Please read our contributing guidelines.
MIT
Inspired by:
- why-did-you-render
- React DevTools Profiler
- The upcoming React Compiler
Built with β€οΈ for React developers who care about performance