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
41 changes: 32 additions & 9 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -1074,7 +1074,14 @@ function getTaskName(type: mixed): string {
}
}

function initializeElement(response: Response, element: any): void {
function initializeElement(
response: Response,
element: any,
lazyType: null | LazyComponent<
React$Element<any>,
SomeChunk<React$Element<any>>,
>,
): void {
if (!__DEV__) {
return;
}
Expand Down Expand Up @@ -1141,6 +1148,18 @@ function initializeElement(response: Response, element: any): void {
if (owner !== null) {
initializeFakeStack(response, owner);
}

// In case the JSX runtime has validated the lazy type as a static child, we
// need to transfer this information to the element.
if (
lazyType &&
lazyType._store &&
lazyType._store.validated &&
!element._store.validated
) {
element._store.validated = lazyType._store.validated;
}

// TODO: We should be freezing the element but currently, we might write into
// _debugInfo later. We could move it into _store which remains mutable.
Object.freeze(element.props);
Expand All @@ -1153,7 +1172,7 @@ function createElement(
props: mixed,
owner: ?ReactComponentInfo, // DEV-only
stack: ?ReactStackTrace, // DEV-only
validated: number, // DEV-only
validated: 0 | 1 | 2, // DEV-only
):
| React$Element<any>
| LazyComponent<React$Element<any>, SomeChunk<React$Element<any>>> {
Expand Down Expand Up @@ -1230,7 +1249,7 @@ function createElement(
handler.reason,
);
if (__DEV__) {
initializeElement(response, element);
initializeElement(response, element, null);
// Conceptually the error happened inside this Element but right before
// it was rendered. We don't have a client side component to render but
// we can add some DebugInfo to explain that this was conceptually a
Expand All @@ -1249,7 +1268,7 @@ function createElement(
}
erroredChunk._debugInfo = [erroredComponent];
}
return createLazyChunkWrapper(erroredChunk);
return createLazyChunkWrapper(erroredChunk, validated);
}
if (handler.deps > 0) {
// We have blocked references inside this Element but we can turn this into
Expand All @@ -1258,23 +1277,25 @@ function createElement(
createBlockedChunk(response);
handler.value = element;
handler.chunk = blockedChunk;
const lazyType = createLazyChunkWrapper(blockedChunk, validated);
if (__DEV__) {
/// After we have initialized any blocked references, initialize stack etc.
const init = initializeElement.bind(null, response, element);
// After we have initialized any blocked references, initialize stack etc.
const init = initializeElement.bind(null, response, element, lazyType);
blockedChunk.then(init, init);
}
return createLazyChunkWrapper(blockedChunk);
return lazyType;
}
}
if (__DEV__) {
initializeElement(response, element);
initializeElement(response, element, null);
}

return element;
}

function createLazyChunkWrapper<T>(
chunk: SomeChunk<T>,
validated: 0 | 1 | 2, // DEV-only
): LazyComponent<T, SomeChunk<T>> {
const lazyType: LazyComponent<T, SomeChunk<T>> = {
$$typeof: REACT_LAZY_TYPE,
Expand All @@ -1286,6 +1307,8 @@ function createLazyChunkWrapper<T>(
const chunkDebugInfo: ReactDebugInfo =
chunk._debugInfo || (chunk._debugInfo = ([]: ReactDebugInfo));
lazyType._debugInfo = chunkDebugInfo;
// Initialize a store for key validation by the JSX runtime.
lazyType._store = {validated: validated};
}
return lazyType;
}
Expand Down Expand Up @@ -2090,7 +2113,7 @@ function parseModelString(
}
// We create a React.lazy wrapper around any lazy values.
// When passed into React, we'll know how to suspend on this.
return createLazyChunkWrapper(chunk);
return createLazyChunkWrapper(chunk, 0);
}
case '@': {
// Promise
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2846,4 +2846,64 @@ describe('ReactFlightDOMBrowser', () => {

expect(container.innerHTML).toBe('<p>Hi</p>');
});

it('should not have missing key warnings when a static child is blocked on debug info', async () => {
const ClientComponent = clientExports(function ClientComponent({element}) {
return (
<div>
<span>Hi</span>
{element}
</div>
);
});

let debugReadableStreamController;

const debugReadableStream = new ReadableStream({
start(controller) {
debugReadableStreamController = controller;
},
});

const stream = await serverAct(() =>
ReactServerDOMServer.renderToReadableStream(
<ClientComponent element={<span>Sebbie</span>} />,
webpackMap,
{
debugChannel: {
writable: new WritableStream({
write(chunk) {
debugReadableStreamController.enqueue(chunk);
},
close() {
debugReadableStreamController.close();
},
}),
},
},
),
);

function ClientRoot({response}) {
return use(response);
}

const response = ReactServerDOMClient.createFromReadableStream(stream, {
debugChannel: {readable: createDelayedStream(debugReadableStream)},
});

const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);

await act(() => {
root.render(<ClientRoot response={response} />);
});

// Wait for the debug info to be processed.
await act(() => {});

expect(container.innerHTML).toBe(
'<div><span>Hi</span><span>Sebbie</span></div>',
);
});
});
3 changes: 3 additions & 0 deletions packages/react/src/ReactLazy.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ export type LazyComponent<T, P> = {
$$typeof: symbol | number,
_payload: P,
_init: (payload: P) => T,

// __DEV__
_debugInfo?: null | ReactDebugInfo,
_store?: {validated: 0 | 1 | 2, ...}, // 0: not validated, 1: validated, 2: force fail
};

function lazyInitializer<T>(payload: Payload<T>): T {
Expand Down
16 changes: 16 additions & 0 deletions packages/react/src/jsx/ReactJSXElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,14 @@ function validateChildKeys(node) {
if (node._store) {
node._store.validated = 1;
}
} else if (isLazyType(node)) {
if (node._payload.status === 'fulfilled') {
if (isValidElement(node._payload.value) && node._payload.value._store) {
node._payload.value._store.validated = 1;
}
} else if (node._store) {
node._store.validated = 1;
}
}
}
}
Expand All @@ -822,3 +830,11 @@ export function isValidElement(object) {
object.$$typeof === REACT_ELEMENT_TYPE
);
}

export function isLazyType(object) {
return (
typeof object === 'object' &&
object !== null &&
object.$$typeof === REACT_LAZY_TYPE
);
}
Loading