Skip to content

Commit aa03948

Browse files
committed
Update error message when only props mismatch
1 parent 60a5328 commit aa03948

File tree

4 files changed

+86
-129
lines changed

4 files changed

+86
-129
lines changed

packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,8 @@ describe('ReactDOMFizzForm', () => {
180180
ReactDOMClient.hydrateRoot(container, <App isClient={true} />);
181181
});
182182
}).toErrorDev(
183-
'Prop `action` did not match. Server: "function" Client: "action"',
183+
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
184+
{withoutStack: true},
184185
);
185186
});
186187

@@ -346,7 +347,12 @@ describe('ReactDOMFizzForm', () => {
346347
await act(async () => {
347348
root = ReactDOMClient.hydrateRoot(container, <App />);
348349
});
349-
}).toErrorDev(['Prop `formTarget` did not match.']);
350+
}).toErrorDev(
351+
[
352+
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
353+
],
354+
{withoutStack: true},
355+
);
350356
await act(async () => {
351357
root.render(<App isUpdate={true} />);
352358
});

packages/react-dom/src/__tests__/ReactDOMRoot-test.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,8 @@ describe('ReactDOMRoot', () => {
171171
</div>,
172172
);
173173
await expect(async () => await waitForAll([])).toErrorDev(
174-
'Extra attribute',
174+
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
175+
{withoutStack: true},
175176
);
176177
});
177178

packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js

Lines changed: 72 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -252,12 +252,6 @@ describe('ReactDOMServerPartialHydration', () => {
252252
});
253253

254254
it('falls back to client rendering boundary on mismatch', async () => {
255-
// We can't use the toErrorDev helper here because this is async.
256-
const originalConsoleError = console.error;
257-
const mockError = jest.fn();
258-
console.error = (...args) => {
259-
mockError(...args.map(normalizeCodeLocInfo));
260-
};
261255
let client = false;
262256
let suspend = false;
263257
let resolve;
@@ -294,77 +288,58 @@ describe('ReactDOMServerPartialHydration', () => {
294288
</Suspense>
295289
);
296290
}
297-
try {
298-
const finalHTML = ReactDOMServer.renderToString(<App />);
299-
const container = document.createElement('section');
300-
container.innerHTML = finalHTML;
301-
assertLog(['Hello', 'Component', 'Component', 'Component', 'Component']);
302-
303-
expect(container.innerHTML).toBe(
304-
'<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
305-
);
291+
const finalHTML = ReactDOMServer.renderToString(<App />);
292+
const container = document.createElement('section');
293+
container.innerHTML = finalHTML;
294+
assertLog(['Hello', 'Component', 'Component', 'Component', 'Component']);
306295

307-
suspend = true;
308-
client = true;
296+
expect(container.innerHTML).toBe(
297+
'<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
298+
);
309299

310-
ReactDOMClient.hydrateRoot(container, <App />, {
311-
onRecoverableError(error) {
312-
Scheduler.log(normalizeError(error.message));
313-
},
314-
});
315-
await waitForAll(['Suspend']);
316-
jest.runAllTimers();
300+
suspend = true;
301+
client = true;
317302

318-
// Unchanged
319-
expect(container.innerHTML).toBe(
320-
'<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
321-
);
303+
ReactDOMClient.hydrateRoot(container, <App />, {
304+
onRecoverableError(error) {
305+
Scheduler.log(normalizeError(error.message));
306+
},
307+
});
308+
await waitForAll(['Suspend']);
309+
jest.runAllTimers();
322310

323-
suspend = false;
324-
resolve();
325-
await promise;
326-
await waitForAll([
327-
// first pass, mismatches at end
328-
'Hello',
329-
'Component',
330-
'Component',
331-
'Component',
332-
'Component',
333-
334-
// second pass as client render
335-
'Hello',
336-
'Component',
337-
'Component',
338-
'Component',
339-
'Component',
340-
341-
// Hydration mismatch is logged
342-
"Hydration failed because the server rendered HTML didn't match the client.",
343-
'There was an error while hydrating this Suspense boundary.',
344-
]);
311+
// Unchanged
312+
expect(container.innerHTML).toBe(
313+
'<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
314+
);
345315

346-
// Client rendered - suspense comment nodes removed
347-
expect(container.innerHTML).toBe(
348-
'Hello<div>Component</div><div>Component</div><div>Component</div><article>Mismatch</article>',
349-
);
316+
suspend = false;
317+
resolve();
318+
await promise;
319+
await waitForAll([
320+
// first pass, mismatches at end
321+
'Hello',
322+
'Component',
323+
'Component',
324+
'Component',
325+
'Component',
326+
327+
// second pass as client render
328+
'Hello',
329+
'Component',
330+
'Component',
331+
'Component',
332+
'Component',
333+
334+
// Hydration mismatch is logged
335+
"Hydration failed because the server rendered HTML didn't match the client.",
336+
'There was an error while hydrating this Suspense boundary.',
337+
]);
350338

351-
if (__DEV__) {
352-
const secondToLastCall =
353-
mockError.mock.calls[mockError.mock.calls.length - 2];
354-
expect(secondToLastCall).toEqual([
355-
'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
356-
'article',
357-
'Suspense',
358-
'\n' +
359-
' in article (at **)\n' +
360-
' in Component (at **)\n' +
361-
' in Suspense (at **)\n' +
362-
' in App (at **)',
363-
]);
364-
}
365-
} finally {
366-
console.error = originalConsoleError;
367-
}
339+
// Client rendered - suspense comment nodes removed
340+
expect(container.innerHTML).toBe(
341+
'Hello<div>Component</div><div>Component</div><div>Component</div><article>Mismatch</article>',
342+
);
368343
});
369344

370345
it('calls the hydration callbacks after hydration or deletion', async () => {
@@ -522,38 +497,27 @@ describe('ReactDOMServerPartialHydration', () => {
522497
expect(container.innerHTML).toContain('<span>B</span>');
523498
expect(ref.current).toBe(null);
524499

525-
await expect(async () => {
526-
await act(() => {
527-
ReactDOMClient.hydrateRoot(container, <App hasB={false} />, {
528-
onRecoverableError(error) {
529-
Scheduler.log(normalizeError(error.message));
530-
},
531-
});
500+
await act(() => {
501+
ReactDOMClient.hydrateRoot(container, <App hasB={false} />, {
502+
onRecoverableError(error) {
503+
Scheduler.log(normalizeError(error.message));
504+
},
532505
});
533-
}).toErrorDev(
534-
'Did not expect server HTML to contain a <span> in <Suspense>',
535-
);
506+
});
536507

537508
expect(container.innerHTML).toContain('<span>A</span>');
538509
expect(container.innerHTML).not.toContain('<span>B</span>');
539510

540511
assertLog([
541512
'Server rendered',
542513
'Client rendered',
543-
'Hydration failed because the initial UI does not match what was rendered on the server.',
514+
"Hydration failed because the server rendered HTML didn't match the client.",
544515
'There was an error while hydrating this Suspense boundary.',
545516
]);
546517
expect(ref.current).not.toBe(span);
547518
});
548519

549520
it('recovers with client render when server rendered additional nodes at suspense root after unsuspending', async () => {
550-
// We can't use the toErrorDev helper here because this is async.
551-
const originalConsoleError = console.error;
552-
const mockError = jest.fn();
553-
console.error = (...args) => {
554-
mockError(...args.map(normalizeCodeLocInfo));
555-
};
556-
557521
const ref = React.createRef();
558522
let shouldSuspend = false;
559523
let resolve;
@@ -581,44 +545,34 @@ describe('ReactDOMServerPartialHydration', () => {
581545
</div>
582546
);
583547
}
584-
try {
585-
const finalHTML = ReactDOMServer.renderToString(<App hasB={true} />);
548+
const finalHTML = ReactDOMServer.renderToString(<App hasB={true} />);
586549

587-
const container = document.createElement('div');
588-
container.innerHTML = finalHTML;
550+
const container = document.createElement('div');
551+
container.innerHTML = finalHTML;
589552

590-
const span = container.getElementsByTagName('span')[0];
553+
const span = container.getElementsByTagName('span')[0];
591554

592-
expect(container.innerHTML).toContain('<span>A</span>');
593-
expect(container.innerHTML).toContain('<span>B</span>');
594-
expect(ref.current).toBe(null);
555+
expect(container.innerHTML).toContain('<span>A</span>');
556+
expect(container.innerHTML).toContain('<span>B</span>');
557+
expect(ref.current).toBe(null);
595558

596-
shouldSuspend = true;
597-
await act(() => {
598-
ReactDOMClient.hydrateRoot(container, <App hasB={false} />);
599-
});
559+
shouldSuspend = true;
560+
await act(() => {
561+
ReactDOMClient.hydrateRoot(container, <App hasB={false} />);
562+
});
600563

564+
await expect(async () => {
601565
await act(() => {
602566
resolve();
603567
});
568+
}).toErrorDev([
569+
"Hydration failed because the server rendered HTML didn't match the client.",
570+
'There was an error while hydrating this Suspense boundary. Switched to client rendering.',
571+
]);
604572

605-
expect(container.innerHTML).toContain('<span>A</span>');
606-
expect(container.innerHTML).not.toContain('<span>B</span>');
607-
expect(ref.current).not.toBe(span);
608-
if (__DEV__) {
609-
expect(mockError).toHaveBeenCalledWith(
610-
'Warning: Did not expect server HTML to contain a <%s> in <%s>.%s',
611-
'span',
612-
'Suspense',
613-
'\n' +
614-
' in Suspense (at **)\n' +
615-
' in div (at **)\n' +
616-
' in App (at **)',
617-
);
618-
}
619-
} finally {
620-
console.error = originalConsoleError;
621-
}
573+
expect(container.innerHTML).toContain('<span>A</span>');
574+
expect(container.innerHTML).not.toContain('<span>B</span>');
575+
expect(ref.current).not.toBe(span);
622576
});
623577

624578
it('recovers with client render when server rendered additional nodes deep inside suspense root', async () => {

packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -251,10 +251,8 @@ describe('ReactDOMServerHydration', () => {
251251
);
252252
});
253253
}).toErrorDev(
254-
'Warning: Prop `style` did not match. Server: ' +
255-
'{"text-decoration":"none","color":"black","height":"10px"}' +
256-
' Client: ' +
257-
'{"textDecoration":"none","color":"white","height":"10px"}',
254+
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
255+
{withoutStack: true},
258256
);
259257
});
260258

@@ -301,10 +299,8 @@ describe('ReactDOMServerHydration', () => {
301299
);
302300
});
303301
}).toErrorDev(
304-
'Warning: Prop `style` did not match. Server: ' +
305-
'{"text-decoration":"none","color":"black","height":"10px"}' +
306-
' Client: ' +
307-
'{"textDecoration":"none","color":"black","height":"10px"}', // note that this is no difference
302+
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
303+
{withoutStack: true},
308304
);
309305
});
310306

0 commit comments

Comments
 (0)