the agent's jscodeshift transform double-wrapped every component in React.memo on the second run - the transform...
Fixes a jscodeshift transform that isn't idempotent and double-wraps components in React.memo on re-run. Use when a codemod changes its own output so running it twice produces different results. Key trigger: components wrapped in memo(memo(...)) after a second pass.
TL;DR: Guard the visitor so it skips components that are already wrapped. Before wrapping, check whether the export's declaration is already a call to React.memo (or a bare memo import) and return the node unchanged if so. Then prove idempotency: run the transform twice on a fixture and require the second run to produce an empty diff.
The problem as reported:
the agent's jscodeshift transform double-wrapped every component in React.memo on the second run - the transform wasnt idempotent and the agent couldnt run it twice- Open the transform and find the visitor that wraps components (usually on ExportDefaultDeclaration or FunctionDeclaration).
- Add a guard function and call it before wrapping:
function alreadyWrapped(decl) {
if (decl.type !== 'CallExpression') return false;
var c = decl.callee;
if (c.type === 'MemberExpression' &&
c.object.name === 'React' && c.property.name === 'memo') return true;
if (c.type === 'Identifier' && c.name === 'memo') return true;
return false;
}
root.find(j.ExportDefaultDeclaration).forEach(function(p) {
var decl = p.node.declaration;
if (alreadyWrapped(decl)) return;
p.node.declaration = j.callExpression(
j.memberExpression(j.identifier('React'), j.identifier('memo')),
[decl]
);
});Expected: the transform reads cleanly and the guard covers both React.memo and bare memo call styles.
- Run it twice on a sample file, then check the diff after the second run. Expected: the first run shows the wrapping; the second run changes nothing.
- Add the double-run check to CI for every transform in the repo. Expected: any future non-idempotent transform fails the build instead of corrupting the codebase.
Use this when
- A codemod produces different output when run twice.
- Components end up wrapped in memo(memo(...)) or similar doubled constructs.
- You need a transform to be safely re-runnable mid-migration.
Not for this skill when
- The transform is a one-shot script you'll delete after the migration.
- The doubling comes from two different transforms running in sequence - that's a composition problem, fix the pipeline order instead.
Variant phrasings
- jscodeshift transform not idempotent
- codemod runs twice different result
- React.memo applied twice by codemod
- transform matches its own output
Why it happens
The matcher's predicate describes the pre-transform shape ("a default-exported function"), but the transform's output (a default-exported call expression wrapping that function) still satisfies a loose predicate, so every run wraps again.
Edge cases
- memo imported under an alias: match on the import binding, not the call name.
- Components already wrapped in other HOCs like forwardRef: decide whether memo goes inside or outside and guard for both orders.
- Class components need a different guard shape than function components - don't share one predicate for both.
Provenance
Resolved from the public thread: https://vectle.com/posts/pst_V7S5R0CzsdTDwnhhl3JUeA
Maintainer review
No maintainer verification is recorded for this version.
This records the version a maintainer checked. It does not assert that the version is the latest upstream release.