Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion packages/react-reconciler/src/ReactFiberLane.js
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,8 @@ export function markRootUpdated(root: FiberRoot, updateLane: Lane) {
// idle updates until after all the regular updates have finished; there's no
// way it could unblock a transition.
if (updateLane !== IdleLane) {
movePendingUpdatersToLane(root, root.pingedLanes, updateLane);

root.suspendedLanes = NoLanes;
root.pingedLanes = NoLanes;
}
Expand Down Expand Up @@ -656,7 +658,8 @@ export function markRootSuspended(
}

export function markRootPinged(root: FiberRoot, pingedLanes: Lanes) {
root.pingedLanes |= root.suspendedLanes & pingedLanes;
// TODO: When would we ever ping lanes that we aren't suspended on?
root.pingedLanes |= pingedLanes;
}

export function markRootFinished(
Expand Down Expand Up @@ -905,6 +908,35 @@ export function addFiberToLanesMap(
}
}

function movePendingUpdatersToLane(
root: FiberRoot,
sourceLanes: Lanes,
targetLane: Lane,
) {
if (!enableUpdaterTracking) {
return;
}
if (!isDevToolsPresent) {
return;
}
const pendingUpdatersLaneMap = root.pendingUpdatersLaneMap;
const targetIndex = laneToIndex(targetLane);
const targetUpdaters = pendingUpdatersLaneMap[targetIndex];
let lanes = sourceLanes;
while (lanes > 0) {
const index = laneToIndex(lanes);
const lane = 1 << index;

const sourceUpdaters = pendingUpdatersLaneMap[index];
sourceUpdaters.forEach(sourceUpdater => {
targetUpdaters.add(sourceUpdater);
});
sourceUpdaters.clear();

lanes &= ~lane;
}
}

export function movePendingFibersToMemoized(root: FiberRoot, lanes: Lanes) {
if (!enableUpdaterTracking) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ describe('updaters', () => {
schedulerTags.push(fiber.tag);
schedulerTypes.push(fiber.elementType);
});
fiberRoot.pendingUpdatersLaneMap.forEach((pendingUpdaters, index) => {
if (pendingUpdaters.size > 0) {
// TODO: Is it ever ok to have dangling pending updaters or is this always a bug?
// const lane = 1 << index;
// throw new Error(
// `Lane ${lane} has pending updaters. Either you didn't assert on all updates in your test or React is leaking updaters.`,
// );
}
});
allSchedulerTags.push(schedulerTags);
allSchedulerTypes.push(schedulerTypes);
}),
Expand Down Expand Up @@ -266,10 +275,7 @@ describe('updaters', () => {
await waitForAll([]);
});

// This test should be convertable to createRoot but the allScheduledTypes assertions are no longer the same
// So I'm leaving it in legacy mode for now and just disabling if legacy mode is turned off
// @gate !disableLegacyMode
it('should cover suspense pings', async () => {
it('should cover suspense pings after update', async () => {
let data = null;
let resolver = null;
let promise = null;
Expand Down Expand Up @@ -303,10 +309,11 @@ describe('updaters', () => {
}
};

const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => {
ReactDOM.render(<Parent />, document.createElement('div'));
assertLog(['onCommitRoot']);
root.render(<Parent />);
});
assertLog(['onCommitRoot']);
expect(setShouldSuspend).not.toBeNull();
expect(allSchedulerTypes).toEqual([[null]]);

Expand All @@ -328,7 +335,63 @@ describe('updaters', () => {
await waitForAll([]);
});

it('should cover suspense pings after mount', async () => {
const {HostRoot} = require('react-reconciler/src/ReactWorkTags');
let data = null;
let resolver = null;
let promise = null;
const fakeCacheRead = () => {
if (data === null) {
promise = new Promise(resolve => {
resolver = resolvedData => {
data = resolvedData;
resolve(resolvedData);
};
});
throw promise;
} else {
return data;
}
};
const Parent = () => (
<React.Suspense fallback={<Fallback />}>
<Suspender />
</React.Suspense>
);
const Fallback = () => null;
const Suspender = ({suspend}) => {
return fakeCacheRead();
};

const root = ReactDOMClient.createRoot(document.createElement('div'));
await act(() => {
root.render(<Parent />);
});
assertLog(['onCommitRoot']);
expect(allSchedulerTags).toEqual([[HostRoot]]);
expect(allSchedulerTypes).toEqual([[null]]);
expect(resolver).not.toBeNull();

allSchedulerTags.length = 0;
allSchedulerTypes.length = 0;
await act(() => {
resolver('abc');
return promise;
});
assertLog(['onCommitRoot']);
expect(allSchedulerTypes).toEqual([
[
// Should be Suspender. `null` will consider the host root as the updater.
null,
],
]);

// Verify no outstanding flushes
await waitForAll([]);
});

it('should cover error handling', async () => {
const {HostRoot} = require('react-reconciler/src/ReactWorkTags');
let triggerError = null;

const Parent = () => {
Expand Down Expand Up @@ -370,14 +433,20 @@ describe('updaters', () => {
});
assertLog(['initial', 'onCommitRoot']);
expect(triggerError).not.toBeNull();
expect(allSchedulerTags).toEqual([[HostRoot]]);
expect(allSchedulerTypes).toEqual([[null]]);

allSchedulerTypes.splice(0);
allSchedulerTags.length = 0;
allSchedulerTypes.length = 0;
onCommitRootShouldYield = true;

await act(() => {
triggerError();
});
// TODO: Why the double commit? Isn't this a single commit?
assertLog(['onCommitRoot', 'error', 'onCommitRoot']);
// TODO: If this was a single commit, ErrorBoundary shouldn't be in here.
// We only set state in Parent so that should be the only updater
expect(allSchedulerTypes).toEqual([[Parent], [ErrorBoundary]]);
Comment on lines +448 to 450
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ErrorBoundary does make sense I believe. It's kind of like a rerender triggered from a child. Though just like the Fiber isn't generally sufficient information for setState updates, the error boundary isn't generally sufficient for "rerenders" due to errors. You'd want the boundary + the origin of the error.


// Verify no outstanding flushes
Expand Down