From 606f76b6e4763581ecf742144ee82756902674ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Mon, 5 Aug 2019 16:36:13 -0700 Subject: [PATCH] Fix hydration bug with nested suspense boundaries (#16288) This happens in this case: `......` getNextHydratableInstanceAfterSuspenseInstance didn't take SUSPENSE_FALLBACK_START_DATA or SUSPENSE_PENDING_START_DATA into account so if a boundary was in one of those states, it wouldn't be considered to push the stack of boundaries. As a result the first end comment was considered the end but it really should've been the second end comment. The next comment then would've been considered something that can be skipped. However, since the logic in there considered all comments as "hydratable", it was considered a hydratable node. Since it was considered a node that didn't actually correspond to anything in the React tree it got deleted. The HTML is now `......` and the trailing content is now hydrated since it did match something. Next, since this was client rendered, we're going to delete the suspended boundary by calling clearSuspenseBoundary and then inserting new content. However, clearSuspenseBoundary *is* aware of SUSPENSE_FALLBACK_START_DATA and assumes that there must be another `` after the first one. As a result it deleted the trailing content from the DOM since it should be part of the boundary. However, those DOM nodes were already hydrated in the React tree. So we end up in an inconsistent state. When we then try to insert the new content we throw as a result. I think we would have recovered correctly if clearSuspenseBoundary and getNextHydratableInstanceAfterSuspenseInstance had the *same* bug but because they were inconsistent we ended up in a bad place. --- ...DOMServerPartialHydration-test.internal.js | 54 +++++++++++++++++++ .../src/client/ReactDOMHostConfig.js | 23 ++++---- 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js index 8f67689f835eb..5b466f2431d96 100644 --- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js +++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js @@ -1003,4 +1003,58 @@ describe('ReactDOMServerPartialHydration', () => { let div = container.getElementsByTagName('div')[0]; expect(ref.current).toBe(div); }); + + it('can client render nested boundaries', async () => { + let suspend = false; + let promise = new Promise(() => {}); + let ref = React.createRef(); + + function Child() { + if (suspend) { + throw promise; + } else { + return 'Hello'; + } + } + + function App() { + return ( +
+ + + + + Inner Sibling + + }> + + + Sibling +
+ ); + } + + suspend = true; + let html = ReactDOMServer.renderToString(); + + let container = document.createElement('div'); + container.innerHTML = html + ''; + + let span = container.getElementsByTagName('span')[1]; + + suspend = false; + let root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + root.render(); + Scheduler.unstable_flushAll(); + jest.runAllTimers(); + + expect(ref.current).toBe(span); + expect(span.parentNode).not.toBe(null); + + // It leaves non-React comments alone. + expect(container.lastChild.nodeType).toBe(8); + expect(container.lastChild.data).toBe('unrelated comment'); + }); }); diff --git a/packages/react-dom/src/client/ReactDOMHostConfig.js b/packages/react-dom/src/client/ReactDOMHostConfig.js index 48425c1c8d930..bc2e94f802d9b 100644 --- a/packages/react-dom/src/client/ReactDOMHostConfig.js +++ b/packages/react-dom/src/client/ReactDOMHostConfig.js @@ -610,15 +610,14 @@ function getNextHydratable(node) { } if (enableSuspenseServerRenderer) { if (nodeType === COMMENT_NODE) { - break; - } - const nodeData = (node: any).data; - if ( - nodeData === SUSPENSE_START_DATA || - nodeData === SUSPENSE_FALLBACK_START_DATA || - nodeData === SUSPENSE_PENDING_START_DATA - ) { - break; + const nodeData = (node: any).data; + if ( + nodeData === SUSPENSE_START_DATA || + nodeData === SUSPENSE_FALLBACK_START_DATA || + nodeData === SUSPENSE_PENDING_START_DATA + ) { + break; + } } } } @@ -691,7 +690,11 @@ export function getNextHydratableInstanceAfterSuspenseInstance( } else { depth--; } - } else if (data === SUSPENSE_START_DATA) { + } else if ( + data === SUSPENSE_START_DATA || + data === SUSPENSE_FALLBACK_START_DATA || + data === SUSPENSE_PENDING_START_DATA + ) { depth++; } }