Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bugfix] Reset subtreeFlags in resetWorkInProgress #20948

Merged
merged 2 commits into from
Mar 7, 2021
Merged
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
189 changes: 189 additions & 0 deletions packages/react-dom/src/__tests__/ReactWrongReturnPointer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,146 @@

let React;
let ReactNoop;
let Scheduler;
let Suspense;
let SuspenseList;
let getCacheForType;
let caches;
let seededCache;

beforeEach(() => {
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');

Suspense = React.Suspense;
SuspenseList = React.unstable_SuspenseList;

getCacheForType = React.unstable_getCacheForType;

caches = [];
seededCache = null;
});

function createTextCache() {
if (seededCache !== null) {
// Trick to seed a cache before it exists.
// TODO: Need a built-in API to seed data before the initial render (i.e.
// not a refresh because nothing has mounted yet).
const cache = seededCache;
seededCache = null;
return cache;
}

const data = new Map();
const version = caches.length + 1;
const cache = {
version,
data,
resolve(text) {
const record = data.get(text);
if (record === undefined) {
const newRecord = {
status: 'resolved',
value: text,
};
data.set(text, newRecord);
} else if (record.status === 'pending') {
const thenable = record.value;
record.status = 'resolved';
record.value = text;
thenable.pings.forEach(t => t());
}
},
reject(text, error) {
const record = data.get(text);
if (record === undefined) {
const newRecord = {
status: 'rejected',
value: error,
};
data.set(text, newRecord);
} else if (record.status === 'pending') {
const thenable = record.value;
record.status = 'rejected';
record.value = error;
thenable.pings.forEach(t => t());
}
},
};
caches.push(cache);
return cache;
}

function readText(text) {
const textCache = getCacheForType(createTextCache);
const record = textCache.data.get(text);
if (record !== undefined) {
switch (record.status) {
case 'pending':
Scheduler.unstable_yieldValue(`Suspend! [${text}]`);
throw record.value;
case 'rejected':
Scheduler.unstable_yieldValue(`Error! [${text}]`);
throw record.value;
case 'resolved':
return textCache.version;
}
} else {
Scheduler.unstable_yieldValue(`Suspend! [${text}]`);

const thenable = {
pings: [],
then(resolve) {
if (newRecord.status === 'pending') {
thenable.pings.push(resolve);
} else {
Promise.resolve().then(() => resolve(newRecord.value));
}
},
};

const newRecord = {
status: 'pending',
value: thenable,
};
textCache.data.set(text, newRecord);

throw thenable;
}
}

function Text({text}) {
Scheduler.unstable_yieldValue(text);
return <span prop={text} />;
}

function AsyncText({text, showVersion}) {
const version = readText(text);
const fullText = showVersion ? `${text} [v${version}]` : text;
Scheduler.unstable_yieldValue(fullText);
return <span prop={fullText} />;
}

// function seedNextTextCache(text) {
// if (seededCache === null) {
// seededCache = createTextCache();
// }
// seededCache.resolve(text);
// }

function resolveMostRecentTextCache(text) {
if (caches.length === 0) {
throw Error('Cache does not exist.');
} else {
// Resolve the most recently created cache. An older cache can by
// resolved with `caches[index].resolve(text)`.
caches[caches.length - 1].resolve(text);
}
}

const resolveText = resolveMostRecentTextCache;

// Don't feel too guilty if you have to delete this test.
// @gate dfsEffectsRefactor
// @gate __DEV__
Expand Down Expand Up @@ -59,3 +193,58 @@ test('warns in DEV if return pointer is inconsistent', async () => {
'Internal React error: Return pointer is inconsistent with parent.',
);
});

// @gate experimental
// @gate enableCache
test('regression (#20932): return pointer is correct before entering deleted tree', async () => {
// Based on a production bug. Designed to trigger a very specific
// implementation path.
function Tail() {
return (
<Suspense fallback={<Text text="Loading Tail..." />}>
<Text text="Tail" />
</Suspense>
);
}

function App() {
return (
<SuspenseList revealOrder="forwards">
<Suspense fallback={<Text text="Loading Async..." />}>
<Async />
</Suspense>
<Tail />
</SuspenseList>
);
}

let setAsyncText;
function Async() {
const [c, _setAsyncText] = React.useState(0);
setAsyncText = _setAsyncText;
return <AsyncText text={c} />;
}

const root = ReactNoop.createRoot();
await ReactNoop.act(async () => {
root.render(<App />);
});
expect(Scheduler).toHaveYielded([
'Suspend! [0]',
'Loading Async...',
'Loading Tail...',
]);
await ReactNoop.act(async () => {
resolveText(0);
});
expect(Scheduler).toHaveYielded([0, 'Tail']);
await ReactNoop.act(async () => {
setAsyncText(x => x + 1);
});
expect(Scheduler).toHaveYielded([
'Suspend! [1]',
'Loading Async...',
'Suspend! [1]',
'Loading Async...',
]);
});
5 changes: 1 addition & 4 deletions packages/react-reconciler/src/ReactFiber.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -388,10 +388,7 @@ export function resetWorkInProgress(workInProgress: Fiber, renderLanes: Lanes) {
workInProgress.lanes = current.lanes;

workInProgress.child = current.child;
// TODO: `subtreeFlags` should be reset to NoFlags, like we do in
// `createWorkInProgress`. Nothing reads this until the complete phase,
// currently, but it might in the future, and we should be consistent.
workInProgress.subtreeFlags = current.subtreeFlags;
workInProgress.subtreeFlags = NoFlags;
workInProgress.deletions = null;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
Expand Down
5 changes: 1 addition & 4 deletions packages/react-reconciler/src/ReactFiber.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -388,10 +388,7 @@ export function resetWorkInProgress(workInProgress: Fiber, renderLanes: Lanes) {
workInProgress.lanes = current.lanes;

workInProgress.child = current.child;
// TODO: `subtreeFlags` should be reset to NoFlags, like we do in
// `createWorkInProgress`. Nothing reads this until the complete phase,
// currently, but it might in the future, and we should be consistent.
workInProgress.subtreeFlags = current.subtreeFlags;
workInProgress.subtreeFlags = NoFlags;
workInProgress.deletions = null;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
Expand Down