Skip to content

Align RefreshSig with React HOC-chain design; make sign idempotent#613

Merged
JoviDeCroock merged 2 commits intomainfrom
fix/refresh-sig-hoc-chain
Apr 15, 2026
Merged

Align RefreshSig with React HOC-chain design; make sign idempotent#613
JoviDeCroock merged 2 commits intomainfrom
fix/refresh-sig-hoc-chain

Conversation

@JoviDeCroock
Copy link
Copy Markdown
Member

Problem

Fixes #610.

Both Babel and Oxc emit two keyed _s calls for a memo-wrapped component:

var _s = $RefreshSig$();
const Button = _s(memo(_c = _s(innerFn, "key")), "key");

The previous $RefreshSig$ used a status state machine. After the inner call, status became 'needsHooks'. When the outer call arrived, sign was invoked with 'needsHooks' on a type that had not been registered yet — crashing with "Cannot set properties of undefined".

The earlier patch (key ? 'begin' : status) stopped the crash, but the underlying design was still fragile: a mutable status variable threading through an HOC chain it was never designed for.

Solution

Align with createSignatureFunctionForTransform from vite-plugin-react/packages/common/refresh-runtime.js, which was explicitly designed for HOC chains:

Before After
Discriminant mutable status typeof key === 'string'
Keyed calls status-dependent always 'begin', regardless of chain depth
Hook collection via 'needsHooks' status didCollectHooks one-shot flag on body call
Re-registration overwrites sign is now idempotent

@prefresh/core's sign is also hardened:

  • 'begin' path: skip if signature already exists (preserves inner getCustomHooks)
  • 'needsHooks' path: guard against missing signature entry

Tests

5 new unit tests in packages/vite/test/refreshSig.test.mjs covering:

  • Plain component (keyed call → body call)
  • No custom hooks (body call is a no-op)
  • Memo HOC chain (both types registered, correct order)
  • Idempotency (inner getCustomHooks not overwritten by outer HOC call)
  • Multiple renders (hook collection fires exactly once)

Package unit tests (vite + rolldown) are now wired into the GitHub Actions workflow.

Both Babel and Oxc emit _s(memo(_c = _s(inner, key)), key) for memo-wrapped
components. The status-machine in the old $RefreshSig$ would call sign with
'needsHooks' on the outer type before it had been registered, crashing with
"Cannot set properties of undefined".

New $RefreshSig$ implementation mirrors createSignatureFunctionForTransform
from vite-plugin-react: use `typeof key === 'string'` to discriminate keyed
vs body calls, always pass 'begin' for keyed calls regardless of chain depth,
and collect hooks once via a didCollectHooks flag on the first body call.

sign() in @prefresh/core is made idempotent on the 'begin' path (won't
overwrite an inner type's getCustomHooks with an outer HOC call's undefined)
and guards the 'needsHooks' path against a missing signature.

Adds unit tests for $RefreshSig$ covering single components, memo-wrapped
HOC chains, and the one-shot hook collection guarantee, and wires package
unit tests into the GitHub Actions workflow.

Fixes #610
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 13, 2026

🦋 Changeset detected

Latest commit: 22f53f5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@prefresh/vite Patch
@prefresh/core Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@JoviDeCroock JoviDeCroock marked this pull request as ready for review April 15, 2026 02:57
@JoviDeCroock JoviDeCroock merged commit 92321b7 into main Apr 15, 2026
1 check passed
@JoviDeCroock JoviDeCroock deleted the fix/refresh-sig-hoc-chain branch April 15, 2026 02:57
@github-actions github-actions Bot mentioned this pull request Apr 15, 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.

Cannot read properties of undefined (reading 'key') caused by memoized components in @prefresh/vite 3.0.0

1 participant