// This import should come before React import
import './react-amnesia-checker.ts';
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(...);
Created
June 23, 2022 08:08
-
-
Save alisey/fa33464f58bd9383033b547af66ac02d to your computer and use it in GitHub Desktop.
Detect React hooks where dependencies change on every render, making memoization ineffective
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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