Skip to content

fix(remove-memoization): replace useMemo with the factory result, preserve untouched imports - #11

Merged
alexbit-codemod merged 5 commits into
codemod:mainfrom
rjolaverria:fix-use-memo-unwrap
Oct 31, 2026
Merged

alexbit-codemod merged 5 commits into
codemod:mainfrom
rjolaverria:fix-use-memo-unwrap

Conversation

@rjolaverria

Copy link
Copy Markdown
Contributor

Fixes #10.

What changed

useMemo is replaced by the factory's result, not the factory. useMemo(factory, deps) evaluates to what the factory returns, but the codemod substituted the factory function itself, so every memoized value became a function. Now:

  • an expression-bodied arrow becomes its body: useMemo(() => ({ a }), [a]) → { a }. Outside declarators, arguments, returns, pairs, arrays, parentheses and JSX expressions the body is parenthesised so precedence survives: 1 + useMemo(() => a * 2) → 1 + (a * 2)
  • a block-bodied or async factory is invoked in place: useMemo(() => { ... }, deps) → (() => { ... })()
  • a bare identifier is called: useMemo(compute, [compute]) → compute()

Import rewriting is narrowed.

  • an import that loses no specifier keeps its original text, which stops import type { X } from 'react' becoming a value import and stops ; being appended in semicolon-free files
  • only the memoization specifiers are candidates for removal; an unrelated unused React import is left alone
  • a rewritten import keeps the file's semicolon style

Fixtures

Six existing expected.tsx files encoded the useMemo bug (a memoized boolean became () => boolean) and are corrected. Three new fixtures cover the expression-body forms, precedence wrapping, async and identifier factories, a type-only import beside a rewritten value import in a semicolon-free file, and an inline type specifier surviving a rewrite.

Verification

  • pnpm test in codemods/remove-memoization: 16 passed
  • pnpm check-types: clean
  • Ran the patched codemod over 13 files of a React 19 + React Compiler app that the published version had broken (18 type errors, 169 failing tests): zero type errors, 687/687 tests passing

🤖 Generated with Claude Code

… the factory

`useMemo(factory, deps)` evaluates to what the factory returns, but the
codemod replaced the call with the factory function itself, so every
memoized value became a function. Context providers received a function
instead of an object, `states.some(...)` threw on a function, and so on.
The `use-memo` fixtures encoded this: a boolean turned into `() => boolean`.

An expression-bodied arrow now becomes its body (parenthesised outside
declarators, arguments, returns, pairs, arrays and JSX expressions so
precedence survives); a block-bodied or async factory is invoked in place
as an IIFE; a bare identifier is called.

Import rewriting is also narrowed. An import that loses no specifier is
left untouched, which stops `import type { X } from 'react'` being
rewritten as a value import and stops a trailing semicolon being added to
semicolon-free files. Only the memoization specifiers themselves are
candidates for removal; unrelated unused React imports are no longer
deleted. A rewritten import keeps the file's semicolon style.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@alexbit-codemod

alexbit-codemod commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

thank you for your contributions @rjolaverria. will check this out soon

Cover parenthesized factories, block bodies that only return an expression,
and preserving an unused default React import while stripping useMemo.

Co-authored-by: Cursor <cursoragent@cursor.com>
@alexbit-codemod

Copy link
Copy Markdown
Contributor

Thanks for this fix — the core useMemo unwrap looks good.

I pushed three additional fixtures that currently fail (cd7b212). Could you update the transform so pnpm test in codemods/remove-memoization is green again?

1. tests/use-memo-parenthesized-factory

Parenthesized expression-bodied arrows should unwrap like bare ones:

useMemo((() => computeValue()), [computeValue])
// expected: computeValue()
// actual today: ((() => computeValue()))()

2. tests/use-memo-block-return

A block body that only returns an expression should become that expression (not an IIFE):

useMemo(() => { return computeValue(); }, [computeValue])
// expected: computeValue()
// actual today: (() => { return computeValue(); })()

3. tests/unused-default-import-preserved

Per the PR description (“an unrelated unused React import is left alone”), an unused default import in a separate statement should stay when only useMemo is removed from another import:

import React from 'react';
import { useMemo, useState } from 'react';
// expected: keep `import React from 'react';`, drop only `useMemo`
// actual today: deletes the unused default import

No rush on polish beyond making these three pass — happy to re-review after.

… factories

- `useMemo((() => x), deps)` unwraps like a bare arrow
- a block body that is only `return <expr>;` becomes that expression;
  blocks with other statements or comments, empty blocks and `function`
  factories are still invoked in place
- sequence-expression results keep their parentheses
- a default or namespace React import is only removed when the codemod
  removed its last uses; an already-unused one is left alone

Adds fixtures for the paths mutation testing (Stryker) showed untested:
blocks kept as IIFEs, sequence results, contexts where the result needs
no parentheses, and removal of a default-only React import.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzRWcG4VtigDwQdDeqTvhk
@rjolaverria

Copy link
Copy Markdown
Contributor Author

Thanks for the fixtures! All three pass now (f22bb65), plus a few more edge-case tests. Ready for another look whenever you have time.

@alexbit-codemod

Copy link
Copy Markdown
Contributor

can you also resolve the conflicts? i recent made some improvements to the codemods by utilizing jssg utils... @rjolaverria

Resolves the remove-memoization conflict with the @jssg/utils import
refactor: keeps upstream's import helpers and ports this branch's
useMemo result unwrapping onto them.

- the unused-default-import rule moves into cleanupUnusedImports: a
  default or namespace import is only removed when the codemod removed
  its last uses
- several memoization specifiers removed from one statement that keeps
  a sibling are cut in non-overlapping edits; per-name removeImport
  edits overlapped and left e.g. `useMemo` behind
- corrects the new no-semicolon-keep-sibling fixture, which encoded the
  useMemo-becomes-a-function bug, and keeps multi-line import formatting
  in type-only-import-preserved
- adds fixtures for multi-specifier removal and a kept default import

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzRWcG4VtigDwQdDeqTvhk
@alexbit-codemod

Copy link
Copy Markdown
Contributor

/codex-review

@github-actions

Copy link
Copy Markdown
Contributor

Codex Review

Reviewed all 46 changed files statically. Five correctness issues remain. No PR code or tests were executed.

Findings

  • [MEDIUM] codemods/remove-memoization/scripts/codemod.ts:195 Keep parameterized arrow factories as invocations
  • [MEDIUM] codemods/remove-memoization/scripts/codemod.ts:198 Preserve TypeScript typing when extracting factory results
  • [MEDIUM] codemods/remove-memoization/scripts/codemod.ts:211 Preserve unbound invocation of member references
  • [MEDIUM] codemods/remove-memoization/scripts/codemod.ts:224 Apply result conversion at every nested memoization layer
  • [MEDIUM] codemods/remove-memoization/scripts/codemod.ts:356 Remove individual import bindings instead of all matching names

@alexbit-codemod

alexbit-codemod commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

i'll take care of the comments above, push the fix, and merge the PR

Address Codex review: keep parameterized factories as invocations, preserve
return/generic types, invoke member factories unbound, convert nested memo
calls inside-out, and remove unused import aliases by local binding. Add
fixtures, update the README/changeset, and ignore local .pnpm-store caches.

Co-authored-by: Cursor <cursoragent@cursor.com>
@alexbit-codemod
alexbit-codemod merged commit 424c5f3 into codemod:main Oct 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

remove-memoization: useMemo is replaced by the factory function, not its result; import type and semicolon style are rewritten

2 participants