@samitouri / QOS-React-1 / commits / df4bd1b4e9

[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,