DefinitelyTyped
DefinitelyTyped copied to clipboard
feat(react): strict compile-time memoization checks
Problem:
React performance can be significantly impacted by unnecessary re-renders. A common cause is using object or function references (created anew on each render) as props to components wrapped in React.memo or using them in hook dependency arrays (useEffect, useLayoutEffect, useCallback, useMemo, etc.). This defeats the purpose of memoization, leading to components re-rendering even when their inputs haven't meaningfully changed.
While static analysis tools like ESLint plugins (eslint-plugin-react-hooks, eslint-plugin-react-usememo) can help detect some of these patterns, they operate at lint time, have limitations based on AST analysis (often missing cross-file issues), can produce false positives/negatives, and only provide suggestions rather than guarantees.
Solution:
This pull request introduces compile-time guarantees for React memoization directly within the TypeScript type definitions for React. By leveraging TypeScript's type system, we can enforce that non-primitive props and dependencies are correctly memoized, catching potential performance issues as you type within your IDE.
Key Benefits:
- โ Compile-Time Safety: Errors are caught during development, not at runtime or lint time.
- ๐ Zero Runtime Overhead: This is purely a type-level enhancement; no changes to runtime behavior or bundle size.
- ๐ก Precision: Leverages TypeScript's full type-checking capabilities, including cross-file analysis, offering more accurate checks than AST-based linters.
- โจ Automatic Enforcement (Even for Libraries!): Because React's core types are augmented, these checks automatically apply to any standard hook โ including those imported from third-party libraries โ without extra configuration. Consumers immediately know if a prop or dependency requires memoization.
- ๐ง Explicit Type Intent: Allows library authors or application developers to explicitly signal (and enforce) that components or custom hooks expect memoized values where necessary, improving API clarity.
- ๐งน Reduces Linter Dependency: Largely eliminates the need for specific ESLint rules (
eslint-plugin-react-usememo, etc.) aimed solely at checking prop/dependency memoization, as TypeScript now handles this enforcement robustly. - ๐ค Improved Developer Experience: Provides immediate feedback in the IDE, leading to faster debugging and more performant code by default.
Implementation Details:
Memoized<T>Branded Type: A nominal typeMemoized<T>(usingT & { readonly __memoized: unknown }) is introduced to mark values explicitly returned byuseMemoanduseCallback.- Type Compatibility:
Memoized<T>is assignable toT. However,Tis only assignable toMemoized<T>ifTis aPrimitive. This ensures non-primitives must be explicitly memoized.
- Type Compatibility:
- Stricter Hook Dependencies: A
MemoizedDependencyListtype replaces the standardDependencyListfor hooks (useEffect,useLayoutEffect,useCallback,useMemo,useImperativeHandle,useInsertionEffect). This list only allows primitives orMemoized<T>values. - Hook Return Types:
useMemo,useCallback, and the state value returned byuseStateare updated to return/be typed asMemoized<T>.
Memo is not covered for now, as the complexity of the types are pushing the limits of TypeScript resolution capabilities. History & Context:
The concept originated with lint-time tools like @arthurgeron/eslint-plugin-react-usememo. An attempt to implement this at the type level via declaration augmentation was made in @arthurgeron/react-memo-types but faced TypeScript declaration merging issues. This PR properly integrates the checks directly into the canonical React types.
BREAKING CHANGE:
Code relying on passing certain unmemoized non-primitive values will now fail type checking:
- Non-primitive values used in hook dependency arrays (useEffect, useMemo, etc.) must be primitive, originate from hooks that return Memoized values (like useState, useMemo, useCallback).
Common patterns like using state directly from
useStatein a dependency array will not break due to this change, asuseState's return value is now typed as memoized. However, values derived from state, refs, values from custom hooks not returningMemoizedtypes, or functions/objects created directly within render scopes will trigger errors if used unmemoized in a dependency array.
Checklist:
- [X] Use a meaningful title for the pull request. Include the name of the package modified. (
feat(react): Add strict compile-time memoization checks) - [X] Test the change in your own code. (Compiled and linted successfully within the repo.)
- [X] Add or edit tests to reflect the change. (
test/hooks.tsxwas updated.) - [X] Follow the advice from the readme.
- [X] Avoid common mistakes.
- [X] Run
pnpm test react. (Ranpnpm -w run lint reactwhich passed.) - [X] Provide a URL to documentation or source code which provides context for the suggested changes:
- Initial ESLint plugin concept: https://github.com/arthurgeron/eslint-plugin-react-usememo
- Previous external augmentation attempt: https://github.com/arthurgeron/react-memo-types
- General React memoization docs: https://react.dev/reference/react/memo, https://react.dev/reference/react/useMemo, https://react.dev/reference/react/useCallback