@samitouri / QOS-React / commits / ba6a9e94ed

[Flight] Warn for keyless fragments in an array (#30588)

Conceptually this is the same as rendering this as if it was a built-in Server Component.

Sebastian Markbåge committed Aug 2, 2024 at 20:08 UTC ba6a9e94edf0db3ad96432804f9931ce9dc89fec
2 files changed +41 -18
packages/react-client/src/__tests__/ReactFlight-test.js
+19
@@ -1632,6 +1632,25 @@ describe('ReactFlight', () => {
1632 }).toErrorDev('Each child in a list should have a unique "key" prop.');
1633 });
1634
1635 + // @gate !__DEV__ || enableOwnerStacks
1636 + it('should warn in DEV a child is missing keys on a fragment', () => {
1637 + expect(() => {
1638 + // While we're on the server we need to have the Server version active to track component stacks.
1639 + jest.resetModules();
1640 + jest.mock('react', () => ReactServer);
1641 + const transport = ReactNoopFlightServer.render(
1642 + ReactServer.createElement(
1643 + 'div',
1644 + null,
1645 + Array(6).fill(ReactServer.createElement(ReactServer.Fragment)),
1646 + ),
1647 + );
1648 + jest.resetModules();
1649 + jest.mock('react', () => React);
1650 + ReactNoopFlightClient.read(transport);
1651 + }).toErrorDev('Each child in a list should have a unique "key" prop.');
1652 + });
1653 +
1654 it('should warn in DEV a child is missing keys in client component', async () => {
1655 function ParentClient({children}) {
1656 return children;
packages/react-server/src/ReactFlightServer.js
+22 -18
@@ -1029,7 +1029,7 @@ function renderFunctionComponent<Props>(
1029 const componentDebugID = debugID;
1030 const componentName =
1031 (Component: any).displayName || Component.name || '';
1032 - const componentEnv = request.environmentName();
1032 + const componentEnv = (0, request.environmentName)();
1033 request.pendingChunks++;
1034 componentDebugInfo = ({
1035 name: componentName,
@@ -1056,14 +1056,8 @@ function renderFunctionComponent<Props>(
1056 // We've emitted the latest environment for this task so we track that.
1057 task.environmentName = componentEnv;
1058
1059 - if (enableOwnerStacks) {
1060 - warnForMissingKey(
1061 - request,
1062 - key,
1063 - validated,
1064 - componentDebugInfo,
1065 - task.debugTask,
1066 - );
1059 + if (enableOwnerStacks && validated === 2) {
1060 + warnForMissingKey(request, key, componentDebugInfo, task.debugTask);
1061 }
1062 }
1063 prepareToUseHooksForComponent(prevThenableState, componentDebugInfo);
@@ -1256,15 +1250,10 @@ function renderFunctionComponent<Props>(
1250 function warnForMissingKey(
1251 request: Request,
1252 key: null | string,
1259 - validated: number,
1253 componentDebugInfo: ReactComponentInfo,
1254 debugTask: null | ConsoleTask,
1255 ): void {
1256 if (__DEV__) {
1264 - if (validated !== 2) {
1265 - return;
1266 - }
1267 -
1257 let didWarnForKey = request.didWarnForKey;
1258 if (didWarnForKey == null) {
1259 didWarnForKey = request.didWarnForKey = new WeakSet();
@@ -1573,6 +1562,21 @@ function renderElement(
1562 } else if (type === REACT_FRAGMENT_TYPE && key === null) {
1563 // For key-less fragments, we add a small optimization to avoid serializing
1564 // it as a wrapper.
1565 + if (__DEV__ && enableOwnerStacks && validated === 2) {
1566 + // Create a fake owner node for the error stack.
1567 + const componentDebugInfo: ReactComponentInfo = {
1568 + name: 'Fragment',
1569 + env: (0, request.environmentName)(),
1570 + owner: task.debugOwner,
1571 + stack:
1572 + task.debugStack === null
1573 + ? null
1574 + : filterStackTrace(request, task.debugStack, 1),
1575 + debugStack: task.debugStack,
1576 + debugTask: task.debugTask,
1577 + };
1578 + warnForMissingKey(request, key, componentDebugInfo, task.debugTask);
1579 + }
1580 const prevImplicitSlot = task.implicitSlot;
1581 if (task.keyPath === null) {
1582 task.implicitSlot = true;
@@ -2921,7 +2925,7 @@ function emitErrorChunk(
2925 if (__DEV__) {
2926 let message;
2927 let stack: ReactStackTrace;
2924 - let env = request.environmentName();
2928 + let env = (0, request.environmentName)();
2929 try {
2930 if (error instanceof Error) {
2931 // eslint-disable-next-line react-internal/safe-string-coercion
@@ -3442,7 +3446,7 @@ function emitConsoleChunk(
3446 }
3447
3448 // TODO: Don't double badge if this log came from another Flight Client.
3445 - const env = request.environmentName();
3449 + const env = (0, request.environmentName)();
3450 const payload = [methodName, stackTrace, owner, env];
3451 // $FlowFixMe[method-unbinding]
3452 payload.push.apply(payload, args);
@@ -3611,7 +3615,7 @@ function retryTask(request: Request, task: Task): void {
3615 request.writtenObjects.set(resolvedModel, serializeByValueID(task.id));
3616
3617 if (__DEV__) {
3614 - const currentEnv = request.environmentName();
3618 + const currentEnv = (0, request.environmentName)();
3619 if (currentEnv !== task.environmentName) {
3620 // The environment changed since we last emitted any debug information for this
3621 // task. We emit an entry that just includes the environment name change.
@@ -3629,7 +3633,7 @@ function retryTask(request: Request, task: Task): void {
3633 const json: string = stringify(resolvedModel);
3634
3635 if (__DEV__) {
3632 - const currentEnv = request.environmentName();
3636 + const currentEnv = (0, request.environmentName)();
3637 if (currentEnv !== task.environmentName) {
3638 // The environment changed since we last emitted any debug information for this
3639 // task. We emit an entry that just includes the environment name change.