[Fast Refresh] Unify hot reload type resolution (#36962)
Previously resolveForwardRefForHotReloading was treated as a special case. It's simpler to move this logic to updateForwardRef which mirrors how updateMemoComponent and mountLazyComponent already are, and it allows us to unify to a single resolveTypeForHotReloading which we'll use in the next commit. Since .type is no longer changing we need to set didReceiveUpdate manually. This behavior is already tested in "can update forwardRef render function in isolation".
Sophie Alpert committed
Jul 8, 2026 at 09:11 UTC
df4bd1b4e99269ac9f3ce3a47ef2e23f76fb1b97
3 files changed
+20
-70
packages/react-reconciler/src/ReactFiber.js
+4
-15
@@ -79,11 +79,7 @@ import {
79
} from './ReactWorkTags';
80
import {getComponentNameFromOwner} from 'react-reconciler/src/getComponentNameFromFiber';
81
import {isDevToolsPresent} from './ReactFiberDevToolsHook';
82
-import {
83
- resolveClassForHotReloading,
84
- resolveFunctionForHotReloading,
85
- resolveForwardRefForHotReloading,
86
-} from './ReactFiberHotReloading';
82
+import {resolveTypeForHotReloading} from './ReactFiberHotReloading';
83
import {NoLanes} from './ReactFiberLane';
84
import {
85
NoMode,
@@ -427,13 +423,9 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
423
switch (workInProgress.tag) {
424
case FunctionComponent:
425
case SimpleMemoComponent:
430
- workInProgress.type = resolveFunctionForHotReloading(current.type);
431
- break;
426
case ClassComponent:
433
- workInProgress.type = resolveClassForHotReloading(current.type);
434
- break;
427
case ForwardRef:
436
- workInProgress.type = resolveForwardRefForHotReloading(current.type);
428
+ workInProgress.type = resolveTypeForHotReloading(current.type);
429
break;
430
default:
431
break;
@@ -573,11 +565,11 @@ export function createFiberFromTypeAndProps(
565
if (shouldConstruct(type)) {
566
fiberTag = ClassComponent;
567
if (__DEV__) {
576
- resolvedType = resolveClassForHotReloading(resolvedType);
568
+ resolvedType = resolveTypeForHotReloading(resolvedType);
569
}
570
} else {
571
if (__DEV__) {
580
- resolvedType = resolveFunctionForHotReloading(resolvedType);
572
+ resolvedType = resolveTypeForHotReloading(resolvedType);
573
}
574
}
575
} else if (typeof type === 'string') {
@@ -664,9 +656,6 @@ export function createFiberFromTypeAndProps(
656
// $FlowFixMe[invalid-compare]
657
case REACT_FORWARD_REF_TYPE:
658
fiberTag = ForwardRef;
667
- if (__DEV__) {
668
- resolvedType = resolveForwardRefForHotReloading(resolvedType);
669
- }
659
break getTag;
660
// $FlowFixMe[invalid-compare]
661
case REACT_MEMO_TYPE:
packages/react-reconciler/src/ReactFiberBeginWork.js
+15
-11
@@ -133,9 +133,7 @@ import {
133
} from 'shared/ReactSymbols';
134
import {setCurrentFiber} from './ReactCurrentFiber';
135
import {
136
- resolveFunctionForHotReloading,
137
- resolveForwardRefForHotReloading,
138
- resolveClassForHotReloading,
136
+ resolveTypeForHotReloading,
137
resolveRemountTypeForHotReloading,
138
} from './ReactFiberHotReloading';
139
@@ -414,7 +412,16 @@ function updateForwardRef(
412
// TODO: current can be non-null here even if the component
413
// hasn't yet mounted. This happens after the first render suspends.
414
// We'll need to figure out if this is fine or can cause issues.
417
- const render = Component.render;
415
+ let render = Component.render;
416
+ if (__DEV__) {
417
+ const resolvedRender = resolveTypeForHotReloading(render);
418
+ if (resolvedRender !== render) {
419
+ render = resolvedRender;
420
+ if (current !== null) {
421
+ didReceiveUpdate = true;
422
+ }
423
+ }
424
+ }
425
const ref = workInProgress.ref;
426
427
let propsWithoutRef;
@@ -482,7 +489,7 @@ function updateMemoComponent(
489
if (isSimpleFunctionComponent(type) && Component.compare === null) {
490
let resolvedType = type;
491
if (__DEV__) {
485
- resolvedType = resolveFunctionForHotReloading(type);
492
+ resolvedType = resolveTypeForHotReloading(type);
493
}
494
// If this is a plain function component without default props,
495
// and with only the default shallow comparison, we upgrade it
@@ -2103,8 +2110,7 @@ function mountLazyComponent(
2110
const resolvedProps = resolveClassComponentProps(Component, props);
2111
workInProgress.tag = ClassComponent;
2112
if (__DEV__) {
2106
- workInProgress.type = Component =
2107
- resolveClassForHotReloading(Component);
2113
+ workInProgress.type = Component = resolveTypeForHotReloading(Component);
2114
}
2115
return updateClassComponent(
2116
null,
@@ -2117,8 +2123,7 @@ function mountLazyComponent(
2123
workInProgress.tag = FunctionComponent;
2124
if (__DEV__) {
2125
validateFunctionComponentInDev(workInProgress, Component);
2120
- workInProgress.type = Component =
2121
- resolveFunctionForHotReloading(Component);
2126
+ workInProgress.type = Component = resolveTypeForHotReloading(Component);
2127
}
2128
return updateFunctionComponent(
2129
null,
@@ -2135,8 +2140,7 @@ function mountLazyComponent(
2140
if ($$typeof === REACT_FORWARD_REF_TYPE) {
2141
workInProgress.tag = ForwardRef;
2142
if (__DEV__) {
2138
- workInProgress.type = Component =
2139
- resolveForwardRefForHotReloading(Component);
2143
+ workInProgress.type = Component = resolveTypeForHotReloading(Component);
2144
}
2145
return updateForwardRef(
2146
null,
packages/react-reconciler/src/ReactFiberHotReloading.js
+1
-44
@@ -61,7 +61,7 @@ export const setRefreshHandler = (handler: RefreshHandler | null): void => {
61
}
62
};
63
64
-export function resolveFunctionForHotReloading(type: any): any {
64
+export function resolveTypeForHotReloading(type: any): any {
65
if (__DEV__) {
66
if (resolveFamily === null) {
67
// Hot reloading is disabled.
@@ -78,49 +78,6 @@ export function resolveFunctionForHotReloading(type: any): any {
78
}
79
}
80
81
-export function resolveClassForHotReloading(type: any): any {
82
- // No implementation differences.
83
- return resolveFunctionForHotReloading(type);
84
-}
85
-
86
-export function resolveForwardRefForHotReloading(type: any): any {
87
- if (__DEV__) {
88
- if (resolveFamily === null) {
89
- // Hot reloading is disabled.
90
- return type;
91
- }
92
- const family = resolveFamily(type);
93
- if (family === undefined) {
94
- // Check if we're dealing with a real forwardRef. Don't want to crash early.
95
- if (
96
- type !== null &&
97
- type !== undefined &&
98
- typeof type.render === 'function'
99
- ) {
100
- // ForwardRef is special because its resolved .type is an object,
101
- // but it's possible that we only have its inner render function in the map.
102
- // If that inner render function is different, we'll build a new forwardRef type.
103
- const currentRender = resolveFunctionForHotReloading(type.render);
104
- if (type.render !== currentRender) {
105
- const syntheticType = {
106
- $$typeof: REACT_FORWARD_REF_TYPE,
107
- render: currentRender,
108
- };
109
- if (type.displayName !== undefined) {
110
- (syntheticType as any).displayName = type.displayName;
111
- }
112
- return syntheticType;
113
- }
114
- }
115
- return type;
116
- }
117
- // Use the latest known implementation.
118
- return family.current;
119
- } else {
120
- return type;
121
- }
122
-}
123
-
81
export function resolveRemountTypeForHotReloading(
82
elementType: any,
83
type: any,