Skip to content

Commit

Permalink
Update error message when only props mismatch
Browse files Browse the repository at this point in the history
  • Loading branch information
sebmarkbage committed Mar 6, 2024
1 parent 60a5328 commit aa03948
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 129 deletions.
10 changes: 8 additions & 2 deletions packages/react-dom/src/__tests__/ReactDOMFizzForm-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,8 @@ describe('ReactDOMFizzForm', () => {
ReactDOMClient.hydrateRoot(container, <App isClient={true} />);
});
}).toErrorDev(
'Prop `action` did not match. Server: "function" Client: "action"',
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
{withoutStack: true},
);
});

Expand Down Expand Up @@ -346,7 +347,12 @@ describe('ReactDOMFizzForm', () => {
await act(async () => {
root = ReactDOMClient.hydrateRoot(container, <App />);
});
}).toErrorDev(['Prop `formTarget` did not match.']);
}).toErrorDev(
[
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
],
{withoutStack: true},
);
await act(async () => {
root.render(<App isUpdate={true} />);
});
Expand Down
3 changes: 2 additions & 1 deletion packages/react-dom/src/__tests__/ReactDOMRoot-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ describe('ReactDOMRoot', () => {
</div>,
);
await expect(async () => await waitForAll([])).toErrorDev(
'Extra attribute',
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
{withoutStack: true},
);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -252,12 +252,6 @@ describe('ReactDOMServerPartialHydration', () => {
});

it('falls back to client rendering boundary on mismatch', async () => {
// We can't use the toErrorDev helper here because this is async.
const originalConsoleError = console.error;
const mockError = jest.fn();
console.error = (...args) => {
mockError(...args.map(normalizeCodeLocInfo));
};
let client = false;
let suspend = false;
let resolve;
Expand Down Expand Up @@ -294,77 +288,58 @@ describe('ReactDOMServerPartialHydration', () => {
</Suspense>
);
}
try {
const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('section');
container.innerHTML = finalHTML;
assertLog(['Hello', 'Component', 'Component', 'Component', 'Component']);

expect(container.innerHTML).toBe(
'<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
);
const finalHTML = ReactDOMServer.renderToString(<App />);
const container = document.createElement('section');
container.innerHTML = finalHTML;
assertLog(['Hello', 'Component', 'Component', 'Component', 'Component']);

suspend = true;
client = true;
expect(container.innerHTML).toBe(
'<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
);

ReactDOMClient.hydrateRoot(container, <App />, {
onRecoverableError(error) {
Scheduler.log(normalizeError(error.message));
},
});
await waitForAll(['Suspend']);
jest.runAllTimers();
suspend = true;
client = true;

// Unchanged
expect(container.innerHTML).toBe(
'<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
);
ReactDOMClient.hydrateRoot(container, <App />, {
onRecoverableError(error) {
Scheduler.log(normalizeError(error.message));
},
});
await waitForAll(['Suspend']);
jest.runAllTimers();

suspend = false;
resolve();
await promise;
await waitForAll([
// first pass, mismatches at end
'Hello',
'Component',
'Component',
'Component',
'Component',

// second pass as client render
'Hello',
'Component',
'Component',
'Component',
'Component',

// Hydration mismatch is logged
"Hydration failed because the server rendered HTML didn't match the client.",
'There was an error while hydrating this Suspense boundary.',
]);
// Unchanged
expect(container.innerHTML).toBe(
'<!--$-->Hello<div>Component</div><div>Component</div><div>Component</div><div>Component</div><!--/$-->',
);

// Client rendered - suspense comment nodes removed
expect(container.innerHTML).toBe(
'Hello<div>Component</div><div>Component</div><div>Component</div><article>Mismatch</article>',
);
suspend = false;
resolve();
await promise;
await waitForAll([
// first pass, mismatches at end
'Hello',
'Component',
'Component',
'Component',
'Component',

// second pass as client render
'Hello',
'Component',
'Component',
'Component',
'Component',

// Hydration mismatch is logged
"Hydration failed because the server rendered HTML didn't match the client.",
'There was an error while hydrating this Suspense boundary.',
]);

if (__DEV__) {
const secondToLastCall =
mockError.mock.calls[mockError.mock.calls.length - 2];
expect(secondToLastCall).toEqual([
'Warning: Expected server HTML to contain a matching <%s> in <%s>.%s',
'article',
'Suspense',
'\n' +
' in article (at **)\n' +
' in Component (at **)\n' +
' in Suspense (at **)\n' +
' in App (at **)',
]);
}
} finally {
console.error = originalConsoleError;
}
// Client rendered - suspense comment nodes removed
expect(container.innerHTML).toBe(
'Hello<div>Component</div><div>Component</div><div>Component</div><article>Mismatch</article>',
);
});

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

await expect(async () => {
await act(() => {
ReactDOMClient.hydrateRoot(container, <App hasB={false} />, {
onRecoverableError(error) {
Scheduler.log(normalizeError(error.message));
},
});
await act(() => {
ReactDOMClient.hydrateRoot(container, <App hasB={false} />, {
onRecoverableError(error) {
Scheduler.log(normalizeError(error.message));
},
});
}).toErrorDev(
'Did not expect server HTML to contain a <span> in <Suspense>',
);
});

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

assertLog([
'Server rendered',
'Client rendered',
'Hydration failed because the initial UI does not match what was rendered on the server.',
"Hydration failed because the server rendered HTML didn't match the client.",
'There was an error while hydrating this Suspense boundary.',
]);
expect(ref.current).not.toBe(span);
});

it('recovers with client render when server rendered additional nodes at suspense root after unsuspending', async () => {
// We can't use the toErrorDev helper here because this is async.
const originalConsoleError = console.error;
const mockError = jest.fn();
console.error = (...args) => {
mockError(...args.map(normalizeCodeLocInfo));
};

const ref = React.createRef();
let shouldSuspend = false;
let resolve;
Expand Down Expand Up @@ -581,44 +545,34 @@ describe('ReactDOMServerPartialHydration', () => {
</div>
);
}
try {
const finalHTML = ReactDOMServer.renderToString(<App hasB={true} />);
const finalHTML = ReactDOMServer.renderToString(<App hasB={true} />);

const container = document.createElement('div');
container.innerHTML = finalHTML;
const container = document.createElement('div');
container.innerHTML = finalHTML;

const span = container.getElementsByTagName('span')[0];
const span = container.getElementsByTagName('span')[0];

expect(container.innerHTML).toContain('<span>A</span>');
expect(container.innerHTML).toContain('<span>B</span>');
expect(ref.current).toBe(null);
expect(container.innerHTML).toContain('<span>A</span>');
expect(container.innerHTML).toContain('<span>B</span>');
expect(ref.current).toBe(null);

shouldSuspend = true;
await act(() => {
ReactDOMClient.hydrateRoot(container, <App hasB={false} />);
});
shouldSuspend = true;
await act(() => {
ReactDOMClient.hydrateRoot(container, <App hasB={false} />);
});

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

expect(container.innerHTML).toContain('<span>A</span>');
expect(container.innerHTML).not.toContain('<span>B</span>');
expect(ref.current).not.toBe(span);
if (__DEV__) {
expect(mockError).toHaveBeenCalledWith(
'Warning: Did not expect server HTML to contain a <%s> in <%s>.%s',
'span',
'Suspense',
'\n' +
' in Suspense (at **)\n' +
' in div (at **)\n' +
' in App (at **)',
);
}
} finally {
console.error = originalConsoleError;
}
expect(container.innerHTML).toContain('<span>A</span>');
expect(container.innerHTML).not.toContain('<span>B</span>');
expect(ref.current).not.toBe(span);
});

it('recovers with client render when server rendered additional nodes deep inside suspense root', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,10 +251,8 @@ describe('ReactDOMServerHydration', () => {
);
});
}).toErrorDev(
'Warning: Prop `style` did not match. Server: ' +
'{"text-decoration":"none","color":"black","height":"10px"}' +
' Client: ' +
'{"textDecoration":"none","color":"white","height":"10px"}',
"A tree hydrated but some attributes of the server rendered HTML didn't match the client properties.",
{withoutStack: true},
);
});

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

Expand Down

0 comments on commit aa03948

Please sign in to comment.