Skip to content

Commit 1e89e1f

Browse files
committed
Warn for keyless fragments in an array
1 parent 06d0b89 commit 1e89e1f

File tree

2 files changed

+45
-18
lines changed

2 files changed

+45
-18
lines changed

packages/react-client/src/__tests__/ReactFlight-test.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1632,6 +1632,24 @@ describe('ReactFlight', () => {
16321632
}).toErrorDev('Each child in a list should have a unique "key" prop.');
16331633
});
16341634

1635+
it('should warn in DEV a child is missing keys on a fragment', () => {
1636+
expect(() => {
1637+
// While we're on the server we need to have the Server version active to track component stacks.
1638+
jest.resetModules();
1639+
jest.mock('react', () => ReactServer);
1640+
const transport = ReactNoopFlightServer.render(
1641+
ReactServer.createElement(
1642+
'div',
1643+
null,
1644+
Array(6).fill(ReactServer.createElement(ReactServer.Fragment)),
1645+
),
1646+
);
1647+
jest.resetModules();
1648+
jest.mock('react', () => React);
1649+
ReactNoopFlightClient.read(transport);
1650+
}).toErrorDev('Each child in a list should have a unique "key" prop.');
1651+
});
1652+
16351653
it('should warn in DEV a child is missing keys in client component', async () => {
16361654
function ParentClient({children}) {
16371655
return children;

packages/react-server/src/ReactFlightServer.js

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,7 +1029,7 @@ function renderFunctionComponent<Props>(
10291029
const componentDebugID = debugID;
10301030
const componentName =
10311031
(Component: any).displayName || Component.name || '';
1032-
const componentEnv = request.environmentName();
1032+
const componentEnv = (0, request.environmentName)();
10331033
request.pendingChunks++;
10341034
componentDebugInfo = ({
10351035
name: componentName,
@@ -1056,14 +1056,8 @@ function renderFunctionComponent<Props>(
10561056
// We've emitted the latest environment for this task so we track that.
10571057
task.environmentName = componentEnv;
10581058

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);
10671061
}
10681062
}
10691063
prepareToUseHooksForComponent(prevThenableState, componentDebugInfo);
@@ -1256,15 +1250,10 @@ function renderFunctionComponent<Props>(
12561250
function warnForMissingKey(
12571251
request: Request,
12581252
key: null | string,
1259-
validated: number,
12601253
componentDebugInfo: ReactComponentInfo,
12611254
debugTask: null | ConsoleTask,
12621255
): void {
12631256
if (__DEV__) {
1264-
if (validated !== 2) {
1265-
return;
1266-
}
1267-
12681257
let didWarnForKey = request.didWarnForKey;
12691258
if (didWarnForKey == null) {
12701259
didWarnForKey = request.didWarnForKey = new WeakSet();
@@ -1573,6 +1562,26 @@ function renderElement(
15731562
} else if (type === REACT_FRAGMENT_TYPE && key === null) {
15741563
// For key-less fragments, we add a small optimization to avoid serializing
15751564
// it as a wrapper.
1565+
if (__DEV__ && enableOwnerStacks && validated === 2) {
1566+
// Create a fake owner node for the error stack.
1567+
const componentDebugInfo = ({
1568+
name: 'Fragment',
1569+
env: (0, request.environmentName)(),
1570+
owner: task.debugOwner,
1571+
}: ReactComponentInfo);
1572+
if (enableOwnerStacks) {
1573+
// $FlowFixMe[cannot-write]
1574+
componentDebugInfo.stack =
1575+
task.debugStack === null
1576+
? null
1577+
: filterStackTrace(request, task.debugStack, 1);
1578+
// $FlowFixMe[cannot-write]
1579+
componentDebugInfo.debugStack = task.debugStack;
1580+
// $FlowFixMe[cannot-write]
1581+
componentDebugInfo.debugTask = task.debugTask;
1582+
}
1583+
warnForMissingKey(request, key, componentDebugInfo, task.debugTask);
1584+
}
15761585
const prevImplicitSlot = task.implicitSlot;
15771586
if (task.keyPath === null) {
15781587
task.implicitSlot = true;
@@ -2921,7 +2930,7 @@ function emitErrorChunk(
29212930
if (__DEV__) {
29222931
let message;
29232932
let stack: ReactStackTrace;
2924-
let env = request.environmentName();
2933+
let env = (0, request.environmentName)();
29252934
try {
29262935
if (error instanceof Error) {
29272936
// eslint-disable-next-line react-internal/safe-string-coercion
@@ -3442,7 +3451,7 @@ function emitConsoleChunk(
34423451
}
34433452

34443453
// TODO: Don't double badge if this log came from another Flight Client.
3445-
const env = request.environmentName();
3454+
const env = (0, request.environmentName)();
34463455
const payload = [methodName, stackTrace, owner, env];
34473456
// $FlowFixMe[method-unbinding]
34483457
payload.push.apply(payload, args);
@@ -3611,7 +3620,7 @@ function retryTask(request: Request, task: Task): void {
36113620
request.writtenObjects.set(resolvedModel, serializeByValueID(task.id));
36123621

36133622
if (__DEV__) {
3614-
const currentEnv = request.environmentName();
3623+
const currentEnv = (0, request.environmentName)();
36153624
if (currentEnv !== task.environmentName) {
36163625
// The environment changed since we last emitted any debug information for this
36173626
// task. We emit an entry that just includes the environment name change.
@@ -3629,7 +3638,7 @@ function retryTask(request: Request, task: Task): void {
36293638
const json: string = stringify(resolvedModel);
36303639

36313640
if (__DEV__) {
3632-
const currentEnv = request.environmentName();
3641+
const currentEnv = (0, request.environmentName)();
36333642
if (currentEnv !== task.environmentName) {
36343643
// The environment changed since we last emitted any debug information for this
36353644
// task. We emit an entry that just includes the environment name change.

0 commit comments

Comments
 (0)