Skip to content

Commit

Permalink
Handle render phase updates explicitly
Browse files Browse the repository at this point in the history
We fire a warning in development if a component is updated during
the render phase (with the exception of local hook updates, which have
their own defined behavior).

Because it's not a supported React pattern, we don't have that many
tests that trigger this path. But it is meant to have reasonable
semantics when it does happen, so that if it accidentally ships to
production, the app doesn't crash unnecessarily. The behavior is not
super well-defined, though.

There are also some _internal_ React implementation details that
intentionally to rely on this behavior. Most prominently, selective
hydration and useOpaqueIdentifier.

I need to tweak the behavior of render phase updates slightly as part
of a fix for useOpaqueIdentifier. This shouldn't cause a user-facing
change in behavior outside of useOpaqueIdentifier, but it does require
that we explicitly model render phase updates.
  • Loading branch information
acdlite committed Oct 13, 2021
1 parent 8ee4ff8 commit c71d3ab
Show file tree
Hide file tree
Showing 2 changed files with 148 additions and 124 deletions.
136 changes: 74 additions & 62 deletions packages/react-reconciler/src/ReactFiberWorkLoop.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -454,86 +454,99 @@ export function scheduleUpdateOnFiber(
eventTime: number,
): FiberRoot | null {
checkForNestedUpdates();
warnAboutRenderPhaseUpdatesInDEV(fiber);

const root = markUpdateLaneFromFiberToRoot(fiber, lane);
if (root === null) {
return null;
}

if (enableUpdaterTracking) {
if (isDevToolsPresent) {
addFiberToLanesMap(root, fiber, lane);
}
}

// Mark that the root has a pending update.
markRootUpdated(root, lane, eventTime);

if (enableProfilerTimer && enableProfilerNestedUpdateScheduledHook) {
if (
(executionContext & CommitContext) !== NoContext &&
root === rootCommittingMutationOrLayoutEffects
) {
if (fiber.mode & ProfileMode) {
let current = fiber;
while (current !== null) {
if (current.tag === Profiler) {
const {id, onNestedUpdateScheduled} = current.memoizedProps;
if (typeof onNestedUpdateScheduled === 'function') {
onNestedUpdateScheduled(id);
if (
(executionContext & RenderContext) !== NoLanes &&
root === workInProgressRoot
) {
// This update was dispatched during the render phase. This is a mistake
// if the update originates from user space (with the exception of local
// hook updates, which are handled differently and don't reach this
// function), but there are some internal React features that use this as
// an implementation detail, like selective hydration
// and useOpaqueIdentifier.
warnAboutRenderPhaseUpdatesInDEV(fiber);
} else {
// This is a normal update, scheduled from outside the render phase. For
// example, during an input event.
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
addFiberToLanesMap(root, fiber, lane);
}
}

if (enableProfilerTimer && enableProfilerNestedUpdateScheduledHook) {
if (
(executionContext & CommitContext) !== NoContext &&
root === rootCommittingMutationOrLayoutEffects
) {
if (fiber.mode & ProfileMode) {
let current = fiber;
while (current !== null) {
if (current.tag === Profiler) {
const {id, onNestedUpdateScheduled} = current.memoizedProps;
if (typeof onNestedUpdateScheduled === 'function') {
onNestedUpdateScheduled(id);
}
}
current = current.return;
}
current = current.return;
}
}
}
}

// TODO: Consolidate with `isInterleavedUpdate` check
if (root === workInProgressRoot) {
// Received an update to a tree that's in the middle of rendering. Mark
// that there was an interleaved update work on this root. Unless the
// `deferRenderPhaseUpdateToNextBatch` flag is off and this is a render
// phase update. In that case, we don't treat render phase updates as if
// they were interleaved, for backwards compat reasons.
// TODO: Consolidate with `isInterleavedUpdate` check
if (root === workInProgressRoot) {
// Received an update to a tree that's in the middle of rendering. Mark
// that there was an interleaved update work on this root. Unless the
// `deferRenderPhaseUpdateToNextBatch` flag is off and this is a render
// phase update. In that case, we don't treat render phase updates as if
// they were interleaved, for backwards compat reasons.
if (
deferRenderPhaseUpdateToNextBatch ||
(executionContext & RenderContext) === NoContext
) {
workInProgressRootUpdatedLanes = mergeLanes(
workInProgressRootUpdatedLanes,
lane,
);
}
if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
// The root already suspended with a delay, which means this render
// definitely won't finish. Since we have a new update, let's mark it as
// suspended now, right before marking the incoming update. This has the
// effect of interrupting the current render and switching to the update.
// TODO: Make sure this doesn't override pings that happen while we've
// already started rendering.
markRootSuspended(root, workInProgressRootRenderLanes);
}
}

ensureRootIsScheduled(root, eventTime);
if (
deferRenderPhaseUpdateToNextBatch ||
(executionContext & RenderContext) === NoContext
lane === SyncLane &&
executionContext === NoContext &&
(fiber.mode & ConcurrentMode) === NoMode &&
// Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
!(__DEV__ && ReactCurrentActQueue.isBatchingLegacy)
) {
workInProgressRootUpdatedLanes = mergeLanes(
workInProgressRootUpdatedLanes,
lane,
);
}
if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
// The root already suspended with a delay, which means this render
// definitely won't finish. Since we have a new update, let's mark it as
// suspended now, right before marking the incoming update. This has the
// effect of interrupting the current render and switching to the update.
// TODO: Make sure this doesn't override pings that happen while we've
// already started rendering.
markRootSuspended(root, workInProgressRootRenderLanes);
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.
resetRenderTimer();
flushSyncCallbacksOnlyInLegacyMode();
}
}

ensureRootIsScheduled(root, eventTime);
if (
lane === SyncLane &&
executionContext === NoContext &&
(fiber.mode & ConcurrentMode) === NoMode &&
// Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
!(__DEV__ && ReactCurrentActQueue.isBatchingLegacy)
) {
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.
resetRenderTimer();
flushSyncCallbacksOnlyInLegacyMode();
}

return root;
}

Expand Down Expand Up @@ -2697,7 +2710,6 @@ function warnAboutRenderPhaseUpdatesInDEV(fiber) {
if (__DEV__) {
if (
ReactCurrentDebugFiberIsRenderingInDEV &&
(executionContext & RenderContext) !== NoContext &&
!getIsUpdatingOpaqueValueInRenderPhaseInDEV()
) {
switch (fiber.tag) {
Expand Down
136 changes: 74 additions & 62 deletions packages/react-reconciler/src/ReactFiberWorkLoop.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -454,86 +454,99 @@ export function scheduleUpdateOnFiber(
eventTime: number,
): FiberRoot | null {
checkForNestedUpdates();
warnAboutRenderPhaseUpdatesInDEV(fiber);

const root = markUpdateLaneFromFiberToRoot(fiber, lane);
if (root === null) {
return null;
}

if (enableUpdaterTracking) {
if (isDevToolsPresent) {
addFiberToLanesMap(root, fiber, lane);
}
}

// Mark that the root has a pending update.
markRootUpdated(root, lane, eventTime);

if (enableProfilerTimer && enableProfilerNestedUpdateScheduledHook) {
if (
(executionContext & CommitContext) !== NoContext &&
root === rootCommittingMutationOrLayoutEffects
) {
if (fiber.mode & ProfileMode) {
let current = fiber;
while (current !== null) {
if (current.tag === Profiler) {
const {id, onNestedUpdateScheduled} = current.memoizedProps;
if (typeof onNestedUpdateScheduled === 'function') {
onNestedUpdateScheduled(id);
if (
(executionContext & RenderContext) !== NoLanes &&
root === workInProgressRoot
) {
// This update was dispatched during the render phase. This is a mistake
// if the update originates from user space (with the exception of local
// hook updates, which are handled differently and don't reach this
// function), but there are some internal React features that use this as
// an implementation detail, like selective hydration
// and useOpaqueIdentifier.
warnAboutRenderPhaseUpdatesInDEV(fiber);
} else {
// This is a normal update, scheduled from outside the render phase. For
// example, during an input event.
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
addFiberToLanesMap(root, fiber, lane);
}
}

if (enableProfilerTimer && enableProfilerNestedUpdateScheduledHook) {
if (
(executionContext & CommitContext) !== NoContext &&
root === rootCommittingMutationOrLayoutEffects
) {
if (fiber.mode & ProfileMode) {
let current = fiber;
while (current !== null) {
if (current.tag === Profiler) {
const {id, onNestedUpdateScheduled} = current.memoizedProps;
if (typeof onNestedUpdateScheduled === 'function') {
onNestedUpdateScheduled(id);
}
}
current = current.return;
}
current = current.return;
}
}
}
}

// TODO: Consolidate with `isInterleavedUpdate` check
if (root === workInProgressRoot) {
// Received an update to a tree that's in the middle of rendering. Mark
// that there was an interleaved update work on this root. Unless the
// `deferRenderPhaseUpdateToNextBatch` flag is off and this is a render
// phase update. In that case, we don't treat render phase updates as if
// they were interleaved, for backwards compat reasons.
// TODO: Consolidate with `isInterleavedUpdate` check
if (root === workInProgressRoot) {
// Received an update to a tree that's in the middle of rendering. Mark
// that there was an interleaved update work on this root. Unless the
// `deferRenderPhaseUpdateToNextBatch` flag is off and this is a render
// phase update. In that case, we don't treat render phase updates as if
// they were interleaved, for backwards compat reasons.
if (
deferRenderPhaseUpdateToNextBatch ||
(executionContext & RenderContext) === NoContext
) {
workInProgressRootUpdatedLanes = mergeLanes(
workInProgressRootUpdatedLanes,
lane,
);
}
if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
// The root already suspended with a delay, which means this render
// definitely won't finish. Since we have a new update, let's mark it as
// suspended now, right before marking the incoming update. This has the
// effect of interrupting the current render and switching to the update.
// TODO: Make sure this doesn't override pings that happen while we've
// already started rendering.
markRootSuspended(root, workInProgressRootRenderLanes);
}
}

ensureRootIsScheduled(root, eventTime);
if (
deferRenderPhaseUpdateToNextBatch ||
(executionContext & RenderContext) === NoContext
lane === SyncLane &&
executionContext === NoContext &&
(fiber.mode & ConcurrentMode) === NoMode &&
// Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
!(__DEV__ && ReactCurrentActQueue.isBatchingLegacy)
) {
workInProgressRootUpdatedLanes = mergeLanes(
workInProgressRootUpdatedLanes,
lane,
);
}
if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
// The root already suspended with a delay, which means this render
// definitely won't finish. Since we have a new update, let's mark it as
// suspended now, right before marking the incoming update. This has the
// effect of interrupting the current render and switching to the update.
// TODO: Make sure this doesn't override pings that happen while we've
// already started rendering.
markRootSuspended(root, workInProgressRootRenderLanes);
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.
resetRenderTimer();
flushSyncCallbacksOnlyInLegacyMode();
}
}

ensureRootIsScheduled(root, eventTime);
if (
lane === SyncLane &&
executionContext === NoContext &&
(fiber.mode & ConcurrentMode) === NoMode &&
// Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
!(__DEV__ && ReactCurrentActQueue.isBatchingLegacy)
) {
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.
resetRenderTimer();
flushSyncCallbacksOnlyInLegacyMode();
}

return root;
}

Expand Down Expand Up @@ -2697,7 +2710,6 @@ function warnAboutRenderPhaseUpdatesInDEV(fiber) {
if (__DEV__) {
if (
ReactCurrentDebugFiberIsRenderingInDEV &&
(executionContext & RenderContext) !== NoContext &&
!getIsUpdatingOpaqueValueInRenderPhaseInDEV()
) {
switch (fiber.tag) {
Expand Down

0 comments on commit c71d3ab

Please sign in to comment.