Skip to content
15 changes: 15 additions & 0 deletions packages/react-client/src/__tests__/ReactFlight-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3884,4 +3884,19 @@ describe('ReactFlight', () => {
</main>,
);
});

// @gate enableOptimisticKey
it('collapses optimistic keys to an optimistic key', async () => {
function Bar({text}) {
return <div />;
}
function Foo() {
return <Bar key={ReactServer.optimisticKey} />;
}
const transport = ReactNoopFlightServer.render({
element: <Foo key="Outer Key" />,
});
const model = await ReactNoopFlightClient.read(transport);
expect(model.element.key).toBe(React.optimisticKey);
});
});
24 changes: 18 additions & 6 deletions packages/react-devtools-shared/src/backend/fiber/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ import {
MEMO_SYMBOL_STRING,
SERVER_CONTEXT_SYMBOL_STRING,
LAZY_SYMBOL_STRING,
REACT_OPTIMISTIC_KEY,
} from '../shared/ReactSymbols';
import {enableStyleXFeatures} from 'react-devtools-feature-flags';

Expand Down Expand Up @@ -4849,7 +4850,10 @@ export function attach(
}
let previousSiblingOfBestMatch = null;
let bestMatch = remainingReconcilingChildren;
if (componentInfo.key != null) {
if (
componentInfo.key != null &&
componentInfo.key !== REACT_OPTIMISTIC_KEY
) {
// If there is a key try to find a matching key in the set.
bestMatch = remainingReconcilingChildren;
while (bestMatch !== null) {
Expand Down Expand Up @@ -6145,7 +6149,7 @@ export function attach(
return {
displayName: getDisplayNameForFiber(fiber) || 'Anonymous',
id: instance.id,
key: fiber.key,
key: fiber.key === REACT_OPTIMISTIC_KEY ? null : fiber.key,
env: null,
stack:
fiber._debugOwner == null || fiber._debugStack == null
Expand All @@ -6158,7 +6162,11 @@ export function attach(
return {
displayName: componentInfo.name || 'Anonymous',
id: instance.id,
key: componentInfo.key == null ? null : componentInfo.key,
key:
componentInfo.key == null ||
componentInfo.key === REACT_OPTIMISTIC_KEY
? null
: componentInfo.key,
env: componentInfo.env == null ? null : componentInfo.env,
stack:
componentInfo.owner == null || componentInfo.debugStack == null
Expand Down Expand Up @@ -7082,7 +7090,7 @@ export function attach(
// Does the component have legacy context attached to it.
hasLegacyContext,

key: key != null ? key : null,
key: key != null && key !== REACT_OPTIMISTIC_KEY ? key : null,

type: elementType,

Expand Down Expand Up @@ -8641,15 +8649,19 @@ export function attach(
}
return {
displayName,
key,
key: key === REACT_OPTIMISTIC_KEY ? null : key,
index,
};
}

function getVirtualPathFrame(virtualInstance: VirtualInstance): PathFrame {
return {
displayName: virtualInstance.data.name || '',
key: virtualInstance.data.key == null ? null : virtualInstance.data.key,
key:
virtualInstance.data.key == null ||
virtualInstance.data.key === REACT_OPTIMISTIC_KEY
? null
: virtualInstance.data.key,
index: -1, // We use -1 to indicate that this is a virtual path frame.
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,9 @@ export const SERVER_CONTEXT_DEFAULT_VALUE_NOT_LOADED_SYMBOL_STRING =
export const REACT_MEMO_CACHE_SENTINEL: symbol = Symbol.for(
'react.memo_cache_sentinel',
);

import type {ReactOptimisticKey} from 'shared/ReactTypes';

export const REACT_OPTIMISTIC_KEY: ReactOptimisticKey = (Symbol.for(
'react.optimistic_key',
): any);
60 changes: 60 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzStaticBrowser-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1111,4 +1111,64 @@ describe('ReactDOMFizzStaticBrowser', () => {
</div>,
);
});

// @gate enableHalt && enableOptimisticKey
it('can resume an optimistic keyed slot', async () => {
const errors = [];

let resolve;
const promise = new Promise(r => (resolve = r));

async function Component() {
await promise;
return 'Hi';
}

if (React.optimisticKey === undefined) {
throw new Error('optimisticKey missing');
}

function App() {
return (
<div>
<Suspense fallback="Loading">
<Component key={React.optimisticKey} />
</Suspense>
</div>
);
}

const controller = new AbortController();
const pendingResult = serverAct(() =>
ReactDOMFizzStatic.prerender(<App />, {
signal: controller.signal,
onError(x) {
errors.push(x.message);
},
}),
);

await serverAct(() => {
controller.abort();
});

const prerendered = await pendingResult;

const postponedState = JSON.stringify(prerendered.postponed);

await readIntoContainer(prerendered.prelude);
expect(getVisibleChildren(container)).toEqual(<div>Loading</div>);

expect(prerendered.postponed).not.toBe(null);

await resolve();

const dynamic = await serverAct(() =>
ReactDOMFizzServer.resume(<App />, JSON.parse(postponedState)),
);

await readIntoContainer(dynamic);

expect(getVisibleChildren(container)).toEqual(<div>Hi</div>);
});
});
Loading
Loading