Don't cut off the tail of a SuspenseList if hydrating (#18854)
Sebastian Markbåge committed
May 6, 2020 at 23:51 UTC
edf6eac8a181860fd8a2d076a43806f1237495a1
3 files changed
+105
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+93
@@ -1698,6 +1698,99 @@ describe('ReactDOMServerPartialHydration', () => {
1698
expect(container.textContent).toBe('ABC');
1699
});
1700
1701
+ // @gate experimental
1702
+ it('clears server boundaries when SuspenseList does a second pass', async () => {
1703
+ let suspend = false;
1704
+ let resolve;
1705
+ const promise = new Promise(resolvePromise => (resolve = resolvePromise));
1706
+
1707
+ const ref = React.createRef();
1708
+
1709
+ function Child({children}) {
1710
+ if (suspend) {
1711
+ throw promise;
1712
+ } else {
1713
+ return children;
1714
+ }
1715
+ }
1716
+
1717
+ function Before() {
1718
+ Scheduler.unstable_yieldValue('Before');
1719
+ return null;
1720
+ }
1721
+
1722
+ function After() {
1723
+ Scheduler.unstable_yieldValue('After');
1724
+ return null;
1725
+ }
1726
+
1727
+ function FirstRow() {
1728
+ return (
1729
+ <>
1730
+ <Before />
1731
+ <Suspense fallback="Loading A">
1732
+ <span>A</span>
1733
+ </Suspense>
1734
+ <After />
1735
+ </>
1736
+ );
1737
+ }
1738
+
1739
+ function App() {
1740
+ return (
1741
+ <Suspense fallback={null}>
1742
+ <SuspenseList revealOrder="forwards" tail="hidden">
1743
+ <FirstRow />
1744
+ <Suspense fallback="Loading B">
1745
+ <Child>
1746
+ <span ref={ref}>B</span>
1747
+ </Child>
1748
+ </Suspense>
1749
+ </SuspenseList>
1750
+ </Suspense>
1751
+ );
1752
+ }
1753
+
1754
+ suspend = false;
1755
+ const html = ReactDOMServer.renderToString(<App />);
1756
+ expect(Scheduler).toHaveYielded(['Before', 'After']);
1757
+
1758
+ const container = document.createElement('div');
1759
+ container.innerHTML = html;
1760
+
1761
+ const b = container.getElementsByTagName('span')[1];
1762
+ expect(b.textContent).toBe('B');
1763
+
1764
+ const root = ReactDOM.createRoot(container, {hydrate: true});
1765
+
1766
+ // Increase hydration priority to higher than "offscreen".
1767
+ ReactDOM.unstable_scheduleHydration(b);
1768
+
1769
+ suspend = true;
1770
+
1771
+ await act(async () => {
1772
+ root.render(<App />);
1773
+ expect(Scheduler).toFlushAndYieldThrough(['Before']);
1774
+ // This took a long time to render.
1775
+ Scheduler.unstable_advanceTime(1000);
1776
+ expect(Scheduler).toFlushAndYield(['After']);
1777
+ // This will cause us to skip the second row completely.
1778
+ });
1779
+
1780
+ // We haven't hydrated the second child but the placeholder is still in the list.
1781
+ expect(ref.current).toBe(null);
1782
+ expect(container.textContent).toBe('AB');
1783
+
1784
+ suspend = false;
1785
+ await act(async () => {
1786
+ // Resolve the boundary to be in its resolved final state.
1787
+ await resolve();
1788
+ });
1789
+
1790
+ expect(container.textContent).toBe('AB');
1791
+ expect(ref.current).toBe(b);
1792
+ });
1793
+
1794
// @gate experimental
1795
it('can client render nested boundaries', async () => {
1796
let suspend = false;
packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+6
@@ -118,6 +118,7 @@ import {
118
prepareToHydrateHostSuspenseInstance,
119
popHydrationState,
120
resetHydrationState,
121
+ getIsHydrating,
122
} from './ReactFiberHydrationContext.new';
123
import {
124
enableSchedulerTracing,
@@ -579,6 +580,11 @@ function cutOffTailIfNeeded(
580
renderState: SuspenseListRenderState,
581
hasRenderedATailFallback: boolean,
582
) {
583
+ if (getIsHydrating()) {
584
+ // If we're hydrating, we should consume as many items as we can
585
+ // so we don't leave any behind.
586
+ return;
587
+ }
588
switch (renderState.tailMode) {
589
case 'hidden': {
590
// Any insertions at the end of the tail list after this point
packages/react-reconciler/src/ReactFiberCompleteWork.old.js
+6
@@ -115,6 +115,7 @@ import {
115
prepareToHydrateHostSuspenseInstance,
116
popHydrationState,
117
resetHydrationState,
118
+ getIsHydrating,
119
} from './ReactFiberHydrationContext.old';
120
import {
121
enableSchedulerTracing,
@@ -575,6 +576,11 @@ function cutOffTailIfNeeded(
576
renderState: SuspenseListRenderState,
577
hasRenderedATailFallback: boolean,
578
) {
579
+ if (getIsHydrating()) {
580
+ // If we're hydrating, we should consume as many items as we can
581
+ // so we don't leave any behind.
582
+ return;
583
+ }
584
switch (renderState.tailMode) {
585
case 'hidden': {
586
// Any insertions at the end of the tail list after this point