@thinksharpe/react-compiler-unmemo
v0.6.1
Published
Remove useMemo and useCallback hooks from React codebases to leverage React Compiler automatic optimization
Maintainers
Readme
react-compiler-unmemo
A codemod that removes useMemo and useCallback from your React codebase so you can adopt React Compiler without the manual cleanup.
Note: React Compiler works fine with existing manual memoization — it preserves and layers on top of it. Removing hooks is optional for cleanliness and readability, but not required for performance. Use this tool to declutter legacy code. Always preview with
--dry-runand type-check afterward.
Quick Start
npx react-compiler-unmemo ./my-react-appThis previews all changes without modifying any files (dry-run is the safe default). When you're ready to apply:
npx react-compiler-unmemo ./my-react-app --writeOr clone and run locally:
git clone https://github.com/thinksharpe/react-compiler-unmemo.git
cd react-compiler-unmemo
npm install
node react-compiler-unmemo.mjs ./path/to/your/projectOrigin
I tried using Claude Opus 4.5 to write a script that would remove useMemo and useCallback from my codebase, but it kept failing. Claude noticed that sed couldn't handle this kind of complex pattern matching — the nested parentheses, arrow functions, and dependency arrays made it impossible with simple text replacement. So it wrote this script instead.
I like to keep my code as readable as possible, and all those useMemo and useCallback hooks were adding extra complexity. I'm glad they're gone from my codebase now. Hope it helps someone else too.
Before / After
- const value = useMemo(() => computeExpensiveValue(a, b), [a, b]);
+ const value = computeExpensiveValue(a, b);
- const handler = useCallback((e) => doSomething(e), [doSomething]);
+ const handler = (e) => doSomething(e);
- import { useMemo, useCallback, useState } from "react";
+ import { useState } from "react";Why
- Cleaner code — less boilerplate, easier to read and maintain
- Aligns with modern React — React Compiler handles memoization automatically
- Smaller bundles — unused hook imports are removed
- Safe to re-run — idempotent, won't touch already-processed files
Usage
# 1. Preview changes (safe default — no files modified)
node react-compiler-unmemo.mjs ./my-app
# 2. Apply changes
node react-compiler-unmemo.mjs ./my-app --write
# 3. Type-check your project
cd ./my-app && npx tsc --noEmitOptions
| Flag | Description | Default |
|------|-------------|--------|
| (no flag) | Preview changes without writing files | dry-run |
| --write | Apply changes to files | off |
| --verbose | Log every transformation | off |
| --files <glob> | Limit to specific file patterns | src/**/*.{tsx,ts} |
| --skip-fix | Skip type annotation repair step | off |
# Only process hooks directory
node react-compiler-unmemo.mjs ./my-app --files "src/hooks/**/*.ts" --write
# app directory
node react-compiler-unmemo.mjs ./my-app --files "app/**/*.{tsx,ts}"
# See every change in detail
node react-compiler-unmemo.mjs ./my-app --verboseWhat It Handles
useMemo(() => expr, [deps])→expruseMemo(() => { return expr; }, [deps])→expruseMemo<Type>(...)→ strips the generic, restores the type annotationuseCallback((params) => body, [deps])→(params) => bodyReact.useMemo/React.useCallbackvariants- Multi-line hooks spanning dozens of lines
- Nested generics like
useMemo<ColumnsType<MyType>>() - Import cleanup (removes unused
useMemo/useCallbackfrom imports)
When NOT to Use
- You haven't enabled React Compiler yet — without the compiler, removing
useMemo/useCallbackmay cause performance regressions - Intentional escape hatches — some hooks are used deliberately to control referential identity for third-party libraries
- Class component interop — if memoized values are passed to class components that rely on shallow comparison
When in doubt, use --dry-run and review the output.
Post-Migration
Always run your type checker after the migration:
npx tsc --noEmit
# or your project's build commandThe tool handles the most common cases automatically, but some things need manual attention:
- Hooks with
//comments inside — the parser may skip or partially transform these. Check the "remaining refs" count in the output. - Missing imports — the type fixer adds
ColumnsType<T>annotations but may not add the import statement. - Dangling
as Typecasts — if auseCallbackhad anas React.FCcast, it may need cleanup. - Broken IIFEs — complex
useMemobodies with comments may leave a trailing, [deps])instead of})().
See docs/edge-cases.md for the full list with code examples.
Project Structure
react-compiler-unmemo/
├── react-compiler-unmemo.mjs # Entry point
├── helpers/
│ ├── remove-hooks.mjs # Core hook removal
│ └── fix-type-annotations.mjs
├── docs/
│ ├── architecture.md # How it works under the hood
│ └── edge-cases.md # Known edge cases & fixes
├── package.json
└── README.mdFor technical details on the parser and pipeline, see docs/architecture.md.
Contributing
Issues, PRs, and edge-case examples welcome. Please open an issue first for large changes.
If you've run this on a real codebase and hit an edge case, sharing the pattern (even without proprietary code) helps improve the tool for everyone.
Links
License
MIT — see LICENSE
