Monkey patches React to notify about avoidable re-renders by tracking pure components and hooks.
—
Advanced hook tracking system for monitoring React hooks like useState, useReducer, and custom hooks to detect unnecessary re-renders caused by hook state changes.
Built-in configuration for tracking standard React hooks:
interface HookConfig {
/** Path to the tracked value within the hook result */
path?: string;
/** Path to dependencies array for memoization hooks */
dependenciesPath?: string;
/** Whether to suppress reporting for this hook type */
dontReport?: boolean;
}
/**
* Built-in hook tracking configuration
*/
const hooksConfig: {
useState: { path: '0' };
useReducer: { path: '0' };
useContext: undefined;
useSyncExternalStore: undefined;
useMemo: { dependenciesPath: '1', dontReport: true };
useCallback: { dependenciesPath: '1', dontReport: true };
};Support for tracking additional hooks beyond the built-in React hooks:
/**
* Configuration for tracking additional hooks
* First element is the hook parent object, second is the hook name
*/
type ExtraHookToTrack = [any, string];Usage Examples:
import whyDidYouRender from '@welldone-software/why-did-you-render';
import { useSelector } from 'react-redux';
// Track React-Redux useSelector hook
whyDidYouRender(React, {
trackHooks: true,
trackExtraHooks: [
[require('react-redux'), 'useSelector']
]
});
// Track custom hooks from your own library
import * as myHooks from './my-custom-hooks';
whyDidYouRender(React, {
trackHooks: true,
trackExtraHooks: [
[myHooks, 'useCustomState'],
[myHooks, 'useAsyncData']
]
});System for detecting and analyzing hook state changes:
interface HookDifference {
/** Path string to the changed value (e.g., "0" for useState result) */
pathString: string;
/** Type of difference detected (e.g., "different", "deepEquals", "same") */
diffType: string;
/** Previous hook value before the change */
prevValue: any;
/** Next hook value after the change */
nextValue: any;
}Internal function that wraps hooks for tracking (exposed for advanced usage):
/**
* Tracks changes in hook results and reports unnecessary re-renders
* @param hookName - Name of the hook being tracked
* @param hookTrackingConfig - Configuration for how to track this hook
* @param rawHookResult - The raw result returned by the hook
* @returns The unmodified hook result
*/
function trackHookChanges(
hookName: string,
hookTrackingConfig: { path?: string },
rawHookResult: any
): any;Data structures for storing hook information during render cycles:
interface HookInfo {
/** Name of the hook */
hookName: string;
/** Current result/value of the hook */
result: any;
}
/**
* Map that stores hook information for the current render cycle
* Keys are component instances, values are arrays of HookInfo
*/
type HooksInfoMap = WeakMap<React.Component, HookInfo[]>;import React, { useState } from 'react';
import whyDidYouRender from '@welldone-software/why-did-you-render';
whyDidYouRender(React, {
trackHooks: true
});
const Counter = () => {
const [count, setCount] = useState(0);
const [name, setName] = useState('Counter');
return (
<div>
<h1>{name}: {count}</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
{/* This will trigger a warning if name doesn't actually change */}
<button onClick={() => setName('Counter')}>Set Same Name</button>
</div>
);
};
Counter.whyDidYouRender = true;import React from 'react';
import { useSelector } from 'react-redux';
import whyDidYouRender from '@welldone-software/why-did-you-render';
whyDidYouRender(React, {
trackHooks: true,
trackExtraHooks: [
[require('react-redux'), 'useSelector']
]
});
const UserProfile = ({ userId }) => {
// This will be tracked for unnecessary re-renders
const user = useSelector(state => state.users[userId]);
const isLoading = useSelector(state => state.ui.loading);
if (isLoading) return <div>Loading...</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
};
UserProfile.whyDidYouRender = true;import React, { useState, useEffect } from 'react';
import whyDidYouRender from '@welldone-software/why-did-you-render';
// Custom hook
const useApi = (url) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(url)
.then(response => response.json())
.then(result => {
setData(result);
setLoading(false);
});
}, [url]);
return { data, loading };
};
// Track the custom hook
whyDidYouRender(React, {
trackHooks: true,
trackExtraHooks: [
[{ useApi }, 'useApi']
]
});
const DataComponent = ({ endpoint }) => {
const { data, loading } = useApi(endpoint);
if (loading) return <div>Loading...</div>;
return <div>{JSON.stringify(data)}</div>;
};
DataComponent.whyDidYouRender = true;interface HookTrackingOptions {
/** Whether to track React hooks for state changes */
trackHooks?: boolean;
/** Additional hooks to track beyond built-in React hooks */
trackExtraHooks?: Array<ExtraHookToTrack>;
}// Example of comprehensive hook tracking setup
whyDidYouRender(React, {
trackHooks: true,
trackExtraHooks: [
// Track React-Redux hooks
[require('react-redux'), 'useSelector'],
[require('react-redux'), 'useDispatch'],
// Track React Router hooks
[require('react-router-dom'), 'useParams'],
[require('react-router-dom'), 'useLocation'],
// Track custom application hooks
[require('./hooks/useAuth'), 'useAuth'],
[require('./hooks/useApi'), 'useApi']
]
});The library categorizes hook changes into different types:
/**
* Types of differences that can be detected in hook values
*/
enum DiffTypes {
/** Values are identical (===) */
same = 'same',
/** Values are different */
different = 'different',
/** Values are deeply equal but not identical */
deepEquals = 'deepEquals'
}For memoization hooks like useMemo and useCallback, the library tracks dependencies:
const MyComponent = ({ items }) => {
// Dependencies array [items.length] will be tracked
const expensiveValue = useMemo(() => {
return items.reduce((sum, item) => sum + item.value, 0);
}, [items.length]); // This dependency array is monitored
return <div>{expensiveValue}</div>;
};For hooks that return arrays or objects, specific paths can be tracked:
// useState returns [value, setter] - path "0" tracks the value
const [count, setCount] = useState(0); // Tracks count changes
// useReducer returns [state, dispatch] - path "0" tracks the state
const [state, dispatch] = useReducer(reducer, initialState); // Tracks state changesYou can provide custom names for better debugging:
whyDidYouRender(React, {
trackHooks: true,
trackExtraHooks: [
[myHooksLibrary, 'useComplexState', { customName: 'ComplexStateHook' }]
]
});Install with Tessl CLI
npx tessl i tessl/npm-welldone-software--why-did-you-render@10.0.1