Skip to content

Instantly share code, notes, and snippets.

@alisey
Created June 23, 2022 08:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alisey/fa33464f58bd9383033b547af66ac02d to your computer and use it in GitHub Desktop.
Save alisey/fa33464f58bd9383033b547af66ac02d to your computer and use it in GitHub Desktop.
Detect React hooks where dependencies change on every render, making memoization ineffective

Usage

// This import should come before React import
import './react-amnesia-checker.ts';

import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(...);
/* eslint-disable no-console */
/* eslint-disable react-hooks/exhaustive-deps */
import React, { DependencyList } from 'react';
const { useMemo, useCallback, memo } = React;
const dependenciesAreEqual = (a: DependencyList, b: DependencyList): any => {
return a.length === b.length && a.every((_, i) => a[i] === b[i]);
};
const objectsAreEqual = (a: Record<string, unknown>, b: Record<string, unknown>): boolean => {
for (const key in a) if (a[key] !== b[key]) return false;
for (const key in b) if (a[key] !== b[key]) return false;
return true;
};
const invalidationCountThreshold = 5;
React.useMemo = (factory, nextDeps) => {
const { current: hookData } = React.useRef<{
invalidationCount: number;
prevDeps: React.DependencyList | undefined;
}>({ invalidationCount: 0, prevDeps: undefined });
if (nextDeps && hookData.prevDeps !== undefined && !dependenciesAreEqual(nextDeps, hookData.prevDeps)) {
hookData.invalidationCount += 1;
if (hookData.invalidationCount === invalidationCountThreshold) {
console.error(`useMemo: dependencies have changed ${invalidationCountThreshold} times in a row`);
}
} else {
hookData.invalidationCount = 0;
}
hookData.prevDeps = nextDeps;
return useMemo(factory, nextDeps);
};
React.useCallback = (callback, nextDeps) => {
const { current: hookData } = React.useRef<{
invalidationCount: number;
prevDeps: React.DependencyList | undefined;
}>({ invalidationCount: 0, prevDeps: undefined });
if (nextDeps && hookData.prevDeps !== undefined && !dependenciesAreEqual(nextDeps, hookData.prevDeps)) {
hookData.invalidationCount += 1;
if (hookData.invalidationCount === invalidationCountThreshold) {
console.error(`useCallback: dependencies have changed ${invalidationCountThreshold} times in a row`);
}
} else {
hookData.invalidationCount = 0;
}
hookData.prevDeps = nextDeps;
return useCallback(callback, nextDeps);
};
React.memo = <P extends Record<string, unknown>>(
Component: React.FunctionComponent<P>,
propsAreEqual?: (prevProps: P, nextProps: P) => boolean
) => {
const Memoized = memo(Component, propsAreEqual);
const componentName = Component.displayName || Component.name;
function MemoWrapper(props: P) {
const { current: instanceData } = React.useRef<{ invalidationCount: number; prevProps?: P }>({
invalidationCount: 0,
prevProps: undefined,
});
if (
instanceData.prevProps !== undefined &&
!(propsAreEqual ?? objectsAreEqual)(instanceData.prevProps, props)
) {
instanceData.invalidationCount += 1;
if (instanceData.invalidationCount === invalidationCountThreshold) {
console.error(
`memo: ${
componentName || (MemoWrapper as any).displayName
} props have changed ${invalidationCountThreshold} times in a row`
);
}
} else {
instanceData.invalidationCount = 0;
}
instanceData.prevProps = props;
return React.createElement(Memoized, props);
}
return MemoWrapper as any;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment