@@ -294,7 +294,9 @@ let workInProgressRootIncludedLanes: Lanes = NoLanes;
294294// includes unprocessed updates, not work in bailed out children.
295295let workInProgressRootSkippedLanes: Lanes = NoLanes;
296296// Lanes that were updated (in an interleaved event) during this render.
297- let workInProgressRootUpdatedLanes: Lanes = NoLanes;
297+ let workInProgressRootInterleavedUpdatedLanes: Lanes = NoLanes;
298+ // Lanes that were updated during the render phase (*not* an interleaved event).
299+ let workInProgressRootRenderPhaseUpdatedLanes: Lanes = NoLanes;
298300// Lanes that were pinged (in an interleaved event) during this render.
299301let workInProgressRootPingedLanes: Lanes = NoLanes;
300302
@@ -454,86 +456,105 @@ export function scheduleUpdateOnFiber(
454456 eventTime : number ,
455457) : FiberRoot | null {
456458 checkForNestedUpdates ( ) ;
457- warnAboutRenderPhaseUpdatesInDEV ( fiber ) ;
458459
459460 const root = markUpdateLaneFromFiberToRoot ( fiber , lane ) ;
460461 if ( root === null ) {
461462 return null ;
462463 }
463464
464- if ( enableUpdaterTracking ) {
465- if ( isDevToolsPresent ) {
466- addFiberToLanesMap ( root , fiber , lane ) ;
467- }
468- }
469-
470465 // Mark that the root has a pending update.
471466 markRootUpdated ( root , lane , eventTime ) ;
472467
473- if ( enableProfilerTimer && enableProfilerNestedUpdateScheduledHook ) {
474- if (
475- ( executionContext & CommitContext ) !== NoContext &&
476- root === rootCommittingMutationOrLayoutEffects
477- ) {
478- if ( fiber . mode & ProfileMode ) {
479- let current = fiber ;
480- while ( current !== null ) {
481- if ( current . tag === Profiler ) {
482- const { id, onNestedUpdateScheduled} = current . memoizedProps ;
483- if ( typeof onNestedUpdateScheduled === 'function' ) {
484- onNestedUpdateScheduled ( id ) ;
468+ if (
469+ ( executionContext & RenderContext ) !== NoLanes &&
470+ root === workInProgressRoot
471+ ) {
472+ // This update was dispatched during the render phase. This is a mistake
473+ // if the update originates from user space (with the exception of local
474+ // hook updates, which are handled differently and don't reach this
475+ // function), but there are some internal React features that use this as
476+ // an implementation detail, like selective hydration
477+ // and useOpaqueIdentifier.
478+ warnAboutRenderPhaseUpdatesInDEV ( fiber ) ;
479+
480+ // Track lanes that were updated during the render phase
481+ workInProgressRootRenderPhaseUpdatedLanes = mergeLanes (
482+ workInProgressRootRenderPhaseUpdatedLanes ,
483+ lane ,
484+ ) ;
485+ } else {
486+ // This is a normal update, scheduled from outside the render phase. For
487+ // example, during an input event.
488+ if ( enableUpdaterTracking ) {
489+ if ( isDevToolsPresent ) {
490+ addFiberToLanesMap ( root , fiber , lane ) ;
491+ }
492+ }
493+
494+ if ( enableProfilerTimer && enableProfilerNestedUpdateScheduledHook ) {
495+ if (
496+ ( executionContext & CommitContext ) !== NoContext &&
497+ root === rootCommittingMutationOrLayoutEffects
498+ ) {
499+ if ( fiber . mode & ProfileMode ) {
500+ let current = fiber ;
501+ while ( current !== null ) {
502+ if ( current . tag === Profiler ) {
503+ const { id, onNestedUpdateScheduled} = current . memoizedProps ;
504+ if ( typeof onNestedUpdateScheduled === 'function' ) {
505+ onNestedUpdateScheduled ( id ) ;
506+ }
485507 }
508+ current = current . return ;
486509 }
487- current = current . return ;
488510 }
489511 }
490512 }
491- }
492513
493- // TODO: Consolidate with `isInterleavedUpdate` check
494- if (root === workInProgressRoot) {
495- // Received an update to a tree that's in the middle of rendering. Mark
496- // that there was an interleaved update work on this root. Unless the
497- // `deferRenderPhaseUpdateToNextBatch` flag is off and this is a render
498- // phase update. In that case, we don't treat render phase updates as if
499- // they were interleaved, for backwards compat reasons.
514+ // TODO: Consolidate with `isInterleavedUpdate` check
515+ if ( root === workInProgressRoot ) {
516+ // Received an update to a tree that's in the middle of rendering. Mark
517+ // that there was an interleaved update work on this root. Unless the
518+ // `deferRenderPhaseUpdateToNextBatch` flag is off and this is a render
519+ // phase update. In that case, we don't treat render phase updates as if
520+ // they were interleaved, for backwards compat reasons.
521+ if (
522+ deferRenderPhaseUpdateToNextBatch ||
523+ ( executionContext & RenderContext ) === NoContext
524+ ) {
525+ workInProgressRootInterleavedUpdatedLanes = mergeLanes (
526+ workInProgressRootInterleavedUpdatedLanes ,
527+ lane ,
528+ ) ;
529+ }
530+ if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
531+ // The root already suspended with a delay, which means this render
532+ // definitely won't finish. Since we have a new update, let's mark it as
533+ // suspended now, right before marking the incoming update. This has the
534+ // effect of interrupting the current render and switching to the update.
535+ // TODO: Make sure this doesn't override pings that happen while we've
536+ // already started rendering.
537+ markRootSuspended ( root , workInProgressRootRenderLanes ) ;
538+ }
539+ }
540+
541+ ensureRootIsScheduled ( root , eventTime ) ;
500542 if (
501- deferRenderPhaseUpdateToNextBatch ||
502- ( executionContext & RenderContext ) === NoContext
543+ lane === SyncLane &&
544+ executionContext === NoContext &&
545+ ( fiber . mode & ConcurrentMode ) === NoMode &&
546+ // Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
547+ ! ( __DEV__ && ReactCurrentActQueue . isBatchingLegacy )
503548 ) {
504- workInProgressRootUpdatedLanes = mergeLanes (
505- workInProgressRootUpdatedLanes ,
506- lane ,
507- ) ;
508- }
509- if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
510- // The root already suspended with a delay, which means this render
511- // definitely won't finish. Since we have a new update, let's mark it as
512- // suspended now, right before marking the incoming update. This has the
513- // effect of interrupting the current render and switching to the update.
514- // TODO: Make sure this doesn't override pings that happen while we've
515- // already started rendering.
516- markRootSuspended ( root , workInProgressRootRenderLanes ) ;
549+ // Flush the synchronous work now, unless we're already working or inside
550+ // a batch. This is intentionally inside scheduleUpdateOnFiber instead of
551+ // scheduleCallbackForFiber to preserve the ability to schedule a callback
552+ // without immediately flushing it. We only do this for user-initiated
553+ // updates, to preserve historical behavior of legacy mode.
554+ resetRenderTimer ( ) ;
555+ flushSyncCallbacksOnlyInLegacyMode ( ) ;
517556 }
518557 }
519-
520- ensureRootIsScheduled ( root , eventTime ) ;
521- if (
522- lane === SyncLane &&
523- executionContext === NoContext &&
524- ( fiber . mode & ConcurrentMode ) === NoMode &&
525- // Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
526- ! ( __DEV__ && ReactCurrentActQueue . isBatchingLegacy )
527- ) {
528- // Flush the synchronous work now, unless we're already working or inside
529- // a batch. This is intentionally inside scheduleUpdateOnFiber instead of
530- // scheduleCallbackForFiber to preserve the ability to schedule a callback
531- // without immediately flushing it. We only do this for user-initiated
532- // updates, to preserve historical behavior of legacy mode.
533- resetRenderTimer ( ) ;
534- flushSyncCallbacksOnlyInLegacyMode ( ) ;
535- }
536-
537558 return root ;
538559}
539560
@@ -865,7 +886,25 @@ function recoverFromConcurrentError(root, errorRetryLanes) {
865886 clearContainer ( root . containerInfo ) ;
866887 }
867888
868- const exitStatus = renderRootSync ( root , errorRetryLanes ) ;
889+ let exitStatus ;
890+
891+ const MAX_ERROR_RETRY_ATTEMPTS = 50 ;
892+ for ( let i = 0 ; i < MAX_ERROR_RETRY_ATTEMPTS ; i ++ ) {
893+ exitStatus = renderRootSync ( root , errorRetryLanes ) ;
894+ if (
895+ exitStatus === RootErrored &&
896+ workInProgressRootRenderPhaseUpdatedLanes !== NoLanes
897+ ) {
898+ // There was a render phase update during this render. This was likely a
899+ // useOpaqueIdentifier hook upgrading itself to a client ID. Try rendering
900+ // again. This time, the component will use a client ID and will proceed
901+ // without throwing. If multiple IDs upgrade as a result of the same
902+ // update, we will have to do multiple render passes. To protect against
903+ // an inifinite loop, eventually we'll give up.
904+ continue;
905+ }
906+ break ;
907+ }
869908
870909 executionContext = prevExecutionContext ;
871910
@@ -1042,7 +1081,10 @@ function markRootSuspended(root, suspendedLanes) {
10421081 // TODO: Lol maybe there's a better way to factor this besides this
10431082 // obnoxiously named function :)
10441083 suspendedLanes = removeLanes ( suspendedLanes , workInProgressRootPingedLanes ) ;
1045- suspendedLanes = removeLanes ( suspendedLanes , workInProgressRootUpdatedLanes ) ;
1084+ suspendedLanes = removeLanes (
1085+ suspendedLanes ,
1086+ workInProgressRootInterleavedUpdatedLanes ,
1087+ ) ;
10461088 markRootSuspended_dontCallThisOneDirectly ( root , suspendedLanes ) ;
10471089}
10481090
@@ -1068,30 +1110,15 @@ function performSyncWorkOnRoot(root) {
10681110
10691111 let exitStatus = renderRootSync(root, lanes);
10701112 if (root.tag !== LegacyRoot && exitStatus === RootErrored ) {
1071- const prevExecutionContext = executionContext ;
1072- executionContext |= RetryAfterError ;
1073-
1074- // If an error occurred during hydration,
1075- // discard server response and fall back to client side render.
1076- if ( root . isDehydrated ) {
1077- root . isDehydrated = false ;
1078- if ( __DEV__ ) {
1079- errorHydratingContainer ( root . containerInfo ) ;
1080- }
1081- clearContainer ( root . containerInfo ) ;
1082- }
1083-
10841113 // If something threw an error, try rendering one more time. We'll render
10851114 // synchronously to block concurrent data mutations, and we'll includes
10861115 // all pending updates are included. If it still fails after the second
10871116 // attempt, we'll give up and commit the resulting tree.
10881117 const errorRetryLanes = getLanesToRetrySynchronouslyOnError ( root ) ;
10891118 if ( errorRetryLanes !== NoLanes ) {
10901119 lanes = errorRetryLanes ;
1091- exitStatus = renderRootSync ( root , lanes ) ;
1120+ exitStatus = recoverFromConcurrentError ( root , errorRetryLanes ) ;
10921121 }
1093-
1094- executionContext = prevExecutionContext;
10951122 }
10961123
10971124 if (exitStatus === RootFatalErrored) {
@@ -1300,7 +1327,8 @@ function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
13001327 workInProgressRootExitStatus = RootIncomplete;
13011328 workInProgressRootFatalError = null;
13021329 workInProgressRootSkippedLanes = NoLanes;
1303- workInProgressRootUpdatedLanes = NoLanes;
1330+ workInProgressRootInterleavedUpdatedLanes = NoLanes;
1331+ workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
13041332 workInProgressRootPingedLanes = NoLanes;
13051333
13061334 enqueueInterleavedUpdates();
@@ -1443,7 +1471,7 @@ export function renderDidSuspendDelayIfPossible(): void {
14431471 if (
14441472 workInProgressRoot !== null &&
14451473 ( includesNonIdleWork ( workInProgressRootSkippedLanes ) ||
1446- includesNonIdleWork ( workInProgressRootUpdatedLanes ) )
1474+ includesNonIdleWork ( workInProgressRootInterleavedUpdatedLanes ) )
14471475 ) {
14481476 // Mark the current render as suspended so that we switch to working on
14491477 // the updates that were skipped. Usually we only suspend at the end of
@@ -2697,7 +2725,6 @@ function warnAboutRenderPhaseUpdatesInDEV(fiber) {
26972725 if ( __DEV__ ) {
26982726 if (
26992727 ReactCurrentDebugFiberIsRenderingInDEV &&
2700- ( executionContext & RenderContext ) !== NoContext &&
27012728 ! getIsUpdatingOpaqueValueInRenderPhaseInDEV ( )
27022729 ) {
27032730 switch ( fiber . tag ) {
0 commit comments