[Fast Refresh] Remount correctly when an edit changes the component kind - #36950
Merged
Conversation
Fixes react#30659. I'm not confident in this yet but here's what Fable said based on react#32214 (comment): Editing a component from a plain function to a memo or forwardRef wrapper (or between wrapper kinds) crashed with "Component is not a function", because canPreserveStateBetween only compared hook signatures. The edit was classified as state-preserving, so createWorkInProgress swapped the wrapper object in as the type of a FunctionComponent fiber and renderWithHooks tried to call it. Two fixes, both DEV-only: - canPreserveStateBetween returns false when typeof or $$typeof differ. A fiber's tag is derived from the kind of its type, so state can never be preserved across a kind change; these edits must go to staleFamilies (remount). Nested wrappers need no special handling because register() creates a family per nesting level ($type/$render ids), and each level's kind is compared against its own family. - The reconciler now finds and remounts wrapper fibers whose outer kind changed. scheduleFibersWithFamiliesRecursively previously resolved families only through the inner implementation (type/type.render), but a kind-changing edit is recorded only on the outer type's family, so edits like memo -> function never reached the scanner and were silently dropped. The scanner now also checks the elementType's family, and the _debugNeedsRemount branch in beginWork rebuilds the fiber from family.current (the latest registered type for the fiber's identity) rather than the fiber's possibly-stale inner type. This also fixes stale simple memo remounts recreating as plain functions, dropping the memo wrapper. Unlike the reverted react#30660 (see react#32214), this does not fabricate families or guess type shapes: families are still created in exactly one place, and the same ID always resolves to the same family. Restores the tests from react#30660 and adds coverage for changing the inner type of a memo between function and forwardRef. Co-authored-by: BIKI DAS <bikid475@gmail.com> Co-authored-by: dan <dan.abramov@me.com>
sophiebits
force-pushed
the
refresh-hoc-kind-change
branch
from
July 7, 2026 06:05
b92afbf to
a686d36
Compare
|
Comparing: fc08d76...a686d36 Critical size changesIncludes critical production bundles, as well as any change greater than 2%:
Significant size changesIncludes any change greater than 0.2%: Expand to show
|
github-actions Bot
pushed a commit
that referenced
this pull request
Jul 7, 2026
…ind (#36950) Fixes #30659. I'm not confident in this yet but here's what Fable said based on #32214 (comment): Editing a component from a plain function to a memo or forwardRef wrapper (or between wrapper kinds) crashed with "Component is not a function", because canPreserveStateBetween only compared hook signatures. The edit was classified as state-preserving, so createWorkInProgress swapped the wrapper object in as the type of a FunctionComponent fiber and renderWithHooks tried to call it. Two fixes, both DEV-only: - canPreserveStateBetween returns false when typeof or $$typeof differ. A fiber's tag is derived from the kind of its type, so state can never be preserved across a kind change; these edits must go to staleFamilies (remount). Nested wrappers need no special handling because register() creates a family per nesting level ($type/$render ids), and each level's kind is compared against its own family. - The reconciler now finds and remounts wrapper fibers whose outer kind changed. scheduleFibersWithFamiliesRecursively previously resolved families only through the inner implementation (type/type.render), but a kind-changing edit is recorded only on the outer type's family, so edits like memo -> function never reached the scanner and were silently dropped. The scanner now also checks the elementType's family, and the _debugNeedsRemount branch in beginWork rebuilds the fiber from family.current (the latest registered type for the fiber's identity) rather than the fiber's possibly-stale inner type. This also fixes stale simple memo remounts recreating as plain functions, dropping the memo wrapper. Unlike the reverted #30660 (see #32214), this does not fabricate families or guess type shapes: families are still created in exactly one place, and the same ID always resolves to the same family. Restores the tests from #30660 and adds coverage for changing the inner type of a memo between function and forwardRef. Co-authored-by: BIKI DAS <bikid475@gmail.com> Co-authored-by: dan <dan.abramov@me.com> DiffTrain build for [eb343c7](eb343c7)
github-actions Bot
pushed a commit
that referenced
this pull request
Jul 7, 2026
…ind (#36950) Fixes #30659. I'm not confident in this yet but here's what Fable said based on #32214 (comment): Editing a component from a plain function to a memo or forwardRef wrapper (or between wrapper kinds) crashed with "Component is not a function", because canPreserveStateBetween only compared hook signatures. The edit was classified as state-preserving, so createWorkInProgress swapped the wrapper object in as the type of a FunctionComponent fiber and renderWithHooks tried to call it. Two fixes, both DEV-only: - canPreserveStateBetween returns false when typeof or $$typeof differ. A fiber's tag is derived from the kind of its type, so state can never be preserved across a kind change; these edits must go to staleFamilies (remount). Nested wrappers need no special handling because register() creates a family per nesting level ($type/$render ids), and each level's kind is compared against its own family. - The reconciler now finds and remounts wrapper fibers whose outer kind changed. scheduleFibersWithFamiliesRecursively previously resolved families only through the inner implementation (type/type.render), but a kind-changing edit is recorded only on the outer type's family, so edits like memo -> function never reached the scanner and were silently dropped. The scanner now also checks the elementType's family, and the _debugNeedsRemount branch in beginWork rebuilds the fiber from family.current (the latest registered type for the fiber's identity) rather than the fiber's possibly-stale inner type. This also fixes stale simple memo remounts recreating as plain functions, dropping the memo wrapper. Unlike the reverted #30660 (see #32214), this does not fabricate families or guess type shapes: families are still created in exactly one place, and the same ID always resolves to the same family. Restores the tests from #30660 and adds coverage for changing the inner type of a memo between function and forwardRef. Co-authored-by: BIKI DAS <bikid475@gmail.com> Co-authored-by: dan <dan.abramov@me.com> DiffTrain build for [eb343c7](eb343c7)
gaearon
reviewed
Jul 7, 2026
| case ClassComponent: | ||
| candidateType = type; | ||
| break; | ||
| case SimpleMemoComponent: |
Contributor
There was a problem hiding this comment.
I wonder what happens if you switch between Memo and SimpleMemo (e.g. by adding areEquals). Maybe worth hardening the test suite to test combination of cases.
Contributor
Author
github-actions Bot
pushed a commit
to code/lib-react
that referenced
this pull request
Jul 9, 2026
…ind (react#36950) Fixes react#30659. I'm not confident in this yet but here's what Fable said based on react#32214 (comment): Editing a component from a plain function to a memo or forwardRef wrapper (or between wrapper kinds) crashed with "Component is not a function", because canPreserveStateBetween only compared hook signatures. The edit was classified as state-preserving, so createWorkInProgress swapped the wrapper object in as the type of a FunctionComponent fiber and renderWithHooks tried to call it. Two fixes, both DEV-only: - canPreserveStateBetween returns false when typeof or $$typeof differ. A fiber's tag is derived from the kind of its type, so state can never be preserved across a kind change; these edits must go to staleFamilies (remount). Nested wrappers need no special handling because register() creates a family per nesting level ($type/$render ids), and each level's kind is compared against its own family. - The reconciler now finds and remounts wrapper fibers whose outer kind changed. scheduleFibersWithFamiliesRecursively previously resolved families only through the inner implementation (type/type.render), but a kind-changing edit is recorded only on the outer type's family, so edits like memo -> function never reached the scanner and were silently dropped. The scanner now also checks the elementType's family, and the _debugNeedsRemount branch in beginWork rebuilds the fiber from family.current (the latest registered type for the fiber's identity) rather than the fiber's possibly-stale inner type. This also fixes stale simple memo remounts recreating as plain functions, dropping the memo wrapper. Unlike the reverted react#30660 (see react#32214), this does not fabricate families or guess type shapes: families are still created in exactly one place, and the same ID always resolves to the same family. Restores the tests from react#30660 and adds coverage for changing the inner type of a memo between function and forwardRef. Co-authored-by: BIKI DAS <bikid475@gmail.com> Co-authored-by: dan <dan.abramov@me.com> DiffTrain build for [eb343c7](react@eb343c7)
github-actions Bot
pushed a commit
to code/lib-react
that referenced
this pull request
Jul 9, 2026
…ind (react#36950) Fixes react#30659. I'm not confident in this yet but here's what Fable said based on react#32214 (comment): Editing a component from a plain function to a memo or forwardRef wrapper (or between wrapper kinds) crashed with "Component is not a function", because canPreserveStateBetween only compared hook signatures. The edit was classified as state-preserving, so createWorkInProgress swapped the wrapper object in as the type of a FunctionComponent fiber and renderWithHooks tried to call it. Two fixes, both DEV-only: - canPreserveStateBetween returns false when typeof or $$typeof differ. A fiber's tag is derived from the kind of its type, so state can never be preserved across a kind change; these edits must go to staleFamilies (remount). Nested wrappers need no special handling because register() creates a family per nesting level ($type/$render ids), and each level's kind is compared against its own family. - The reconciler now finds and remounts wrapper fibers whose outer kind changed. scheduleFibersWithFamiliesRecursively previously resolved families only through the inner implementation (type/type.render), but a kind-changing edit is recorded only on the outer type's family, so edits like memo -> function never reached the scanner and were silently dropped. The scanner now also checks the elementType's family, and the _debugNeedsRemount branch in beginWork rebuilds the fiber from family.current (the latest registered type for the fiber's identity) rather than the fiber's possibly-stale inner type. This also fixes stale simple memo remounts recreating as plain functions, dropping the memo wrapper. Unlike the reverted react#30660 (see react#32214), this does not fabricate families or guess type shapes: families are still created in exactly one place, and the same ID always resolves to the same family. Restores the tests from react#30660 and adds coverage for changing the inner type of a memo between function and forwardRef. Co-authored-by: BIKI DAS <bikid475@gmail.com> Co-authored-by: dan <dan.abramov@me.com> DiffTrain build for [eb343c7](react@eb343c7)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #30659. I'm not confident in this yet but here's what Fable said based on #32214 (comment):
Editing a component from a plain function to a memo or forwardRef wrapper (or between wrapper kinds) crashed with "Component is not a function", because canPreserveStateBetween only compared hook signatures. The edit was classified as state-preserving, so createWorkInProgress swapped the wrapper object in as the type of a FunctionComponent fiber and renderWithHooks tried to call it.
Two fixes, both DEV-only:
canPreserveStateBetween returns false when typeof or $$typeof differ. A fiber's tag is derived from the kind of its type, so state can never be preserved across a kind change; these edits must go to staleFamilies (remount). Nested wrappers need no special handling because register() creates a family per nesting level ($type/$render ids), and each level's kind is compared against its own family.
The reconciler now finds and remounts wrapper fibers whose outer kind changed. scheduleFibersWithFamiliesRecursively previously resolved families only through the inner implementation (type/type.render), but a kind-changing edit is recorded only on the outer type's family, so edits like memo -> function never reached the scanner and were silently dropped. The scanner now also checks the elementType's family, and the _debugNeedsRemount branch in beginWork rebuilds the fiber from family.current (the latest registered type for the fiber's identity) rather than the fiber's possibly-stale inner type. This also fixes stale simple memo remounts recreating as plain functions, dropping the memo wrapper.
Unlike the reverted #30660 (see #32214), this does not fabricate families or guess type shapes: families are still created in exactly one place, and the same ID always resolves to the same family.
Restores the tests from #30660 and adds coverage for changing the inner type of a memo between function and forwardRef.