Skip to content
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
25 changes: 14 additions & 11 deletions fixtures/view-transition/src/components/Page.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,17 +171,20 @@ export default function Page({url, navigate}) {
}}>
<h1>{!show ? 'A' + counter : 'B'}</h1>
</ViewTransition>
{show ? (
<div>
{a}
{b}
</div>
) : (
<div>
{b}
{a}
</div>
)}
{
// Using url instead of renderedUrl here lets us only update this on commit.
url === '/?b' ? (
<div>
{a}
{b}
</div>
) : (
<div>
{b}
{a}
</div>
)
}
<ViewTransition>
{show ? (
<div>hello{exclamation}</div>
Expand Down
11 changes: 6 additions & 5 deletions fixtures/view-transition/src/components/SwipeRecognizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,17 @@ export default function SwipeRecognizer({
);
}
function onGestureEnd(changed) {
// Reset scroll
if (changed) {
// Trigger side-effects
startTransition(action);
}
// We cancel the gesture before invoking side-effects to allow the gesture lane to fully commit
// before scheduling new updates.
if (activeGesture.current !== null) {
const cancelGesture = activeGesture.current;
activeGesture.current = null;
cancelGesture();
}
if (changed) {
// Trigger side-effects
startTransition(action);
}
}
function onScrollEnd() {
if (touchTimeline.current) {
Expand Down
142 changes: 142 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9478,4 +9478,146 @@ Unfortunately that previous paragraph wasn't quite long enough so I'll continue
</div>,
);
});

it('useId is consistent for siblings when component suspends with nested lazy', async () => {
// Inner component uses useId
function InnerComponent() {
const id = React.useId();
Scheduler.log('InnerComponent id: ' + id);
return <span id={id}>inner</span>;
}

// Outer component uses useId and renders a lazy inner
function OuterComponent({innerElement}) {
const id = React.useId();
Scheduler.log('OuterComponent id: ' + id);
return <div id={id}>{innerElement}</div>;
}

// This sibling also has useId - its ID must be consistent with server
function Sibling() {
const id = React.useId();
Scheduler.log('Sibling id: ' + id);
return <span id={id}>sibling</span>;
}

// Create fresh lazy components for SERVER (resolve immediately)
const serverLazyInner = React.lazy(async () => {
Scheduler.log('server lazy inner initializer');
return {default: <InnerComponent />};
});

const serverLazyOuter = React.lazy(async () => {
Scheduler.log('server lazy outer initializer');
return {
default: <OuterComponent key="outer" innerElement={serverLazyInner} />,
};
});

// Server render with lazy (resolves immediately)
await act(() => {
const {pipe} = renderToPipeableStream(
<html>
<body>
<>{serverLazyOuter}</>
<>
<Sibling />
</>
</body>
</html>,
);
pipe(writable);
});

expect(getVisibleChildren(document)).toEqual(
<html>
<head />
<body>
<div id="_R_1_">
<span id="_R_5_">inner</span>
</div>
<span id="_R_2_">sibling</span>
</body>
</html>,
);

assertLog([
'server lazy outer initializer',
'Sibling id: _R_2_',
'OuterComponent id: _R_1_',
'server lazy inner initializer',
'InnerComponent id: _R_5_',
]);

// Create fresh lazy components for CLIENT
let resolveClientInner;
const clientLazyInner = React.lazy(async () => {
Scheduler.log('client lazy inner initializer');
return new Promise(r => {
resolveClientInner = () => r({default: <InnerComponent />});
});
});

let resolveClientOuter;
const clientLazyOuter = React.lazy(async () => {
Scheduler.log('client lazy outer initializer');
return new Promise(r => {
resolveClientOuter = () =>
r({default: <OuterComponent innerElement={clientLazyInner} />});
});
});

const hydrationErrors = [];

// Client hydrates with nested lazy components
let root;
React.startTransition(() => {
root = ReactDOMClient.hydrateRoot(
document,
<html>
<body>
<>{clientLazyOuter}</>
<>
<Sibling />
</>
</body>
</html>,
{
onRecoverableError(error) {
hydrationErrors.push(error.message);
},
},
);
});

// First suspension on outer lazy
await waitFor(['client lazy outer initializer']);
resolveClientOuter();

// Second suspension on inner lazy
await waitFor([
'OuterComponent id: _R_1_',
'client lazy inner initializer',
]);
resolveClientInner();

await waitForAll(['InnerComponent id: _R_5_', 'Sibling id: _R_2_']);

// The IDs should match the server-generated IDs
expect(hydrationErrors).toEqual([]);

expect(getVisibleChildren(document)).toEqual(
<html>
<head />
<body>
<div id="_R_1_">
<span id="_R_5_">inner</span>
</div>
<span id="_R_2_">sibling</span>
</body>
</html>,
);

root.unmount();
});
});
5 changes: 3 additions & 2 deletions packages/react-reconciler/src/ReactFiberFlags.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@ export const ShouldCapture = /* */ 0b0000000000000010000000000000
export const ForceUpdateForLegacySuspense = /* */ 0b0000000000000100000000000000000;
export const DidPropagateContext = /* */ 0b0000000000001000000000000000000;
export const NeedsPropagation = /* */ 0b0000000000010000000000000000000;
export const Forked = /* */ 0b0000000000100000000000000000000;

// Static tags describe aspects of a fiber that are not specific to a render,
// e.g. a fiber uses a passive effect (even if there are no updates on this particular render).
// This enables us to defer more work in the unmount case,
// since we can defer traversing the tree during layout to look for Passive effects,
// and instead rely on the static flag as a signal that there may be cleanup work.
export const Forked = /* */ 0b0000000000100000000000000000000;
export const SnapshotStatic = /* */ 0b0000000001000000000000000000000;
export const LayoutStatic = /* */ 0b0000000010000000000000000000000;
export const RefStatic = LayoutStatic;
Expand Down Expand Up @@ -142,4 +142,5 @@ export const StaticMask =
MaySuspendCommit |
ViewTransitionStatic |
ViewTransitionNamedStatic |
PortalStatic;
PortalStatic |
Forked;
Loading
Loading