Skip to content

[pull] main from facebook:main #156

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

Merged
merged 3 commits into from
May 20, 2025
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
14 changes: 0 additions & 14 deletions compiler/.gitignore
Original file line number Diff line number Diff line change
@@ -1,28 +1,14 @@
.DS_Store
.spr.yml

# Generated by Cargo
# will have compiled files and executables
debug/
target/

# These are backup files generated by rustfmt
**/*.rs.bk

# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

node_modules
.watchmanconfig
.watchman-cookie-*
dist
.vscode
!packages/playground/.vscode
.spr.yml
testfilter.txt

bundle-oss.sh

# forgive
*.vsix
.vscode-test
12 changes: 0 additions & 12 deletions compiler/scripts/rustfmt.sh

This file was deleted.

73 changes: 73 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzSuspenseList-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -324,4 +324,77 @@ describe('ReactDOMFizSuspenseList', () => {
</div>,
);
});

// @gate enableSuspenseList
it('waits for a nested SuspenseList to complete before resolving "forwards"', async () => {
const A = createAsyncText('A');
const B = createAsyncText('B');
const C = createAsyncText('C');

function Foo() {
return (
<div>
<SuspenseList revealOrder="forwards">
<SuspenseList revealOrder="backwards">
<Suspense fallback={<Text text="Loading A" />}>
<A />
</Suspense>
<Suspense fallback={<Text text="Loading B" />}>
<B />
</Suspense>
</SuspenseList>
<Suspense fallback={<Text text="Loading C" />}>
<C />
</Suspense>
</SuspenseList>
</div>
);
}

await C.resolve();

await serverAct(async () => {
const {pipe} = ReactDOMFizzServer.renderToPipeableStream(<Foo />);
pipe(writable);
});

assertLog([
'Suspend! [B]',
'Suspend! [A]',
'C',
'Loading B',
'Loading A',
'Loading C',
]);

expect(getVisibleChildren(container)).toEqual(
<div>
<span>Loading A</span>
<span>Loading B</span>
<span>Loading C</span>
</div>,
);

await serverAct(() => A.resolve());
assertLog(['A']);

expect(getVisibleChildren(container)).toEqual(
<div>
<span>Loading A</span>
<span>Loading B</span>
<span>Loading C</span>
</div>,
);

await serverAct(() => B.resolve());
assertLog(['B']);

expect(getVisibleChildren(container)).toEqual(
<div>
<span>A</span>
<span>B</span>
<span>C</span>
</div>,
);
});
});
101 changes: 101 additions & 0 deletions packages/react-reconciler/src/ReactChildFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
Thenable,
ReactContext,
ReactDebugInfo,
SuspenseListRevealOrder,
} from 'shared/ReactTypes';
import type {Fiber} from './ReactInternalTypes';
import type {Lanes} from './ReactFiberLane';
Expand Down Expand Up @@ -2057,3 +2058,103 @@ export function resetChildFibers(workInProgress: Fiber, lanes: Lanes): void {
child = child.sibling;
}
}

function validateSuspenseListNestedChild(childSlot: mixed, index: number) {
if (__DEV__) {
const isAnArray = isArray(childSlot);
const isIterable =
!isAnArray && typeof getIteratorFn(childSlot) === 'function';
const isAsyncIterable =
enableAsyncIterableChildren &&
typeof childSlot === 'object' &&
childSlot !== null &&
typeof (childSlot: any)[ASYNC_ITERATOR] === 'function';
if (isAnArray || isIterable || isAsyncIterable) {
const type = isAnArray
? 'array'
: isAsyncIterable
? 'async iterable'
: 'iterable';
console.error(
'A nested %s was passed to row #%s in <SuspenseList />. Wrap it in ' +
'an additional SuspenseList to configure its revealOrder: ' +
'<SuspenseList revealOrder=...> ... ' +
'<SuspenseList revealOrder=...>{%s}</SuspenseList> ... ' +
'</SuspenseList>',
type,
index,
type,
);
return false;
}
}
return true;
}

export function validateSuspenseListChildren(
children: mixed,
revealOrder: SuspenseListRevealOrder,
) {
if (__DEV__) {
if (
(revealOrder === 'forwards' || revealOrder === 'backwards') &&
children !== undefined &&
children !== null &&
children !== false
) {
if (isArray(children)) {
for (let i = 0; i < children.length; i++) {
if (!validateSuspenseListNestedChild(children[i], i)) {
return;
}
}
} else {
const iteratorFn = getIteratorFn(children);
if (typeof iteratorFn === 'function') {
const childrenIterator = iteratorFn.call(children);
if (childrenIterator) {
let step = childrenIterator.next();
let i = 0;
for (; !step.done; step = childrenIterator.next()) {
if (!validateSuspenseListNestedChild(step.value, i)) {
return;
}
i++;
}
}
} else if (
enableAsyncIterableChildren &&
typeof (children: any)[ASYNC_ITERATOR] === 'function'
) {
// TODO: Technically we should warn for nested arrays inside the
// async iterable but it would require unwrapping the array.
// However, this mistake is not as easy to make so it's ok not to warn.
} else if (
enableAsyncIterableChildren &&
children.$$typeof === REACT_ELEMENT_TYPE &&
typeof children.type === 'function' &&
// $FlowFixMe
(Object.prototype.toString.call(children.type) ===
'[object GeneratorFunction]' ||
// $FlowFixMe
Object.prototype.toString.call(children.type) ===
'[object AsyncGeneratorFunction]')
) {
console.error(
'A generator Component was passed to a <SuspenseList revealOrder="%s" />. ' +
'This is not supported as a way to generate lists. Instead, pass an ' +
'iterable as the children.',
revealOrder,
);
} else {
console.error(
'A single row was passed to a <SuspenseList revealOrder="%s" />. ' +
'This is not useful since it needs multiple rows. ' +
'Did you mean to pass multiple children or an array?',
revealOrder,
);
}
}
}
}
}
89 changes: 12 additions & 77 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,6 @@ import {
enableViewTransition,
enableFragmentRefs,
} from 'shared/ReactFeatureFlags';
import isArray from 'shared/isArray';
import shallowEqual from 'shared/shallowEqual';
import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';
import getComponentNameFromType from 'shared/getComponentNameFromType';
Expand All @@ -132,7 +131,6 @@ import {
REACT_LAZY_TYPE,
REACT_FORWARD_REF_TYPE,
REACT_MEMO_TYPE,
getIteratorFn,
} from 'shared/ReactSymbols';
import {setCurrentFiber} from './ReactCurrentFiber';
import {
Expand All @@ -145,6 +143,7 @@ import {
mountChildFibers,
reconcileChildFibers,
cloneChildFibers,
validateSuspenseListChildren,
} from './ReactChildFiber';
import {
processUpdateQueue,
Expand Down Expand Up @@ -3302,73 +3301,6 @@ function validateTailOptions(
}
}

function validateSuspenseListNestedChild(childSlot: mixed, index: number) {
if (__DEV__) {
const isAnArray = isArray(childSlot);
const isIterable =
!isAnArray && typeof getIteratorFn(childSlot) === 'function';
if (isAnArray || isIterable) {
const type = isAnArray ? 'array' : 'iterable';
console.error(
'A nested %s was passed to row #%s in <SuspenseList />. Wrap it in ' +
'an additional SuspenseList to configure its revealOrder: ' +
'<SuspenseList revealOrder=...> ... ' +
'<SuspenseList revealOrder=...>{%s}</SuspenseList> ... ' +
'</SuspenseList>',
type,
index,
type,
);
return false;
}
}
return true;
}

function validateSuspenseListChildren(
children: mixed,
revealOrder: SuspenseListRevealOrder,
) {
if (__DEV__) {
if (
(revealOrder === 'forwards' || revealOrder === 'backwards') &&
children !== undefined &&
children !== null &&
children !== false
) {
if (isArray(children)) {
for (let i = 0; i < children.length; i++) {
if (!validateSuspenseListNestedChild(children[i], i)) {
return;
}
}
} else {
const iteratorFn = getIteratorFn(children);
if (typeof iteratorFn === 'function') {
const childrenIterator = iteratorFn.call(children);
if (childrenIterator) {
let step = childrenIterator.next();
let i = 0;
for (; !step.done; step = childrenIterator.next()) {
if (!validateSuspenseListNestedChild(step.value, i)) {
return;
}
i++;
}
}
} else {
console.error(
'A single row was passed to a <SuspenseList revealOrder="%s" />. ' +
'This is not useful since it needs multiple rows. ' +
'Did you mean to pass multiple children or an array?',
revealOrder,
);
}
}
}
}
}

function initSuspenseListRenderState(
workInProgress: Fiber,
isBackwards: boolean,
Expand Down Expand Up @@ -3415,12 +3347,6 @@ function updateSuspenseListComponent(
const tailMode: SuspenseListTailMode = nextProps.tail;
const newChildren = nextProps.children;

validateRevealOrder(revealOrder);
validateTailOptions(tailMode, revealOrder);
validateSuspenseListChildren(newChildren, revealOrder);

reconcileChildren(current, workInProgress, newChildren, renderLanes);

let suspenseContext: SuspenseContext = suspenseStackCursor.current;

const shouldForceFallback = hasSuspenseListContext(
Expand All @@ -3434,6 +3360,17 @@ function updateSuspenseListComponent(
);
workInProgress.flags |= DidCapture;
} else {
suspenseContext = setDefaultShallowSuspenseListContext(suspenseContext);
}
pushSuspenseListContext(workInProgress, suspenseContext);

validateRevealOrder(revealOrder);
validateTailOptions(tailMode, revealOrder);
validateSuspenseListChildren(newChildren, revealOrder);

reconcileChildren(current, workInProgress, newChildren, renderLanes);

if (!shouldForceFallback) {
const didSuspendBefore =
current !== null && (current.flags & DidCapture) !== NoFlags;
if (didSuspendBefore) {
Expand All @@ -3446,9 +3383,7 @@ function updateSuspenseListComponent(
renderLanes,
);
}
suspenseContext = setDefaultShallowSuspenseListContext(suspenseContext);
}
pushSuspenseListContext(workInProgress, suspenseContext);

if (!disableLegacyMode && (workInProgress.mode & ConcurrentMode) === NoMode) {
// In legacy mode, SuspenseList doesn't work so we just
Expand Down
Loading
Loading