Skip to content

Commit 691b93b

Browse files
committed
Mark the render as delayed if we have to retry
This allows the suspense config to kick in and we can wait for much longer before we're forced to give up on hydrating.
1 parent c0b08de commit 691b93b

File tree

3 files changed

+93
-1
lines changed

3 files changed

+93
-1
lines changed

fixtures/ssr/src/components/Chrome.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,16 @@ export default class Chrome extends Component {
2626
<Theme.Provider value={this.state.theme}>
2727
{this.props.children}
2828
<div>
29-
<ThemeToggleButton onChange={theme => this.setState({theme})} />
29+
<ThemeToggleButton
30+
onChange={theme => {
31+
React.unstable_withSuspenseConfig(
32+
() => {
33+
this.setState({theme});
34+
},
35+
{timeoutMs: 6000}
36+
);
37+
}}
38+
/>
3039
</div>
3140
</Theme.Provider>
3241
<script

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

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,86 @@ describe('ReactDOMServerPartialHydration', () => {
560560
expect(container.textContent).toBe('Hi Hi');
561561
});
562562

563+
it('hydrates first if props changed but we are able to resolve within a timeout', async () => {
564+
let suspend = false;
565+
let resolve;
566+
let promise = new Promise(resolvePromise => (resolve = resolvePromise));
567+
let ref = React.createRef();
568+
569+
function Child({text}) {
570+
if (suspend) {
571+
throw promise;
572+
} else {
573+
return text;
574+
}
575+
}
576+
577+
function App({text, className}) {
578+
return (
579+
<div>
580+
<Suspense fallback="Loading...">
581+
<span ref={ref} className={className}>
582+
<Child text={text} />
583+
</span>
584+
</Suspense>
585+
</div>
586+
);
587+
}
588+
589+
suspend = false;
590+
let finalHTML = ReactDOMServer.renderToString(
591+
<App text="Hello" className="hello" />,
592+
);
593+
let container = document.createElement('div');
594+
container.innerHTML = finalHTML;
595+
596+
let span = container.getElementsByTagName('span')[0];
597+
598+
// On the client we don't have all data yet but we want to start
599+
// hydrating anyway.
600+
suspend = true;
601+
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
602+
root.render(<App text="Hello" className="hello" />);
603+
Scheduler.unstable_flushAll();
604+
jest.runAllTimers();
605+
606+
expect(ref.current).toBe(null);
607+
expect(container.textContent).toBe('Hello');
608+
609+
// Render an update with a long timeout.
610+
React.unstable_withSuspenseConfig(
611+
() => root.render(<App text="Hi" className="hi" />),
612+
{timeoutMs: 5000},
613+
);
614+
615+
// This shouldn't force the fallback yet.
616+
Scheduler.unstable_flushAll();
617+
618+
expect(ref.current).toBe(null);
619+
expect(container.textContent).toBe('Hello');
620+
621+
// Resolving the promise so that rendering can complete.
622+
suspend = false;
623+
resolve();
624+
await promise;
625+
626+
// This should first complete the hydration and then flush the update onto the hydrated state.
627+
Scheduler.unstable_flushAll();
628+
jest.runAllTimers();
629+
630+
// The new span should be the same since we should have successfully hydrated
631+
// before changing it.
632+
let newSpan = container.getElementsByTagName('span')[0];
633+
expect(span).toBe(newSpan);
634+
635+
// We should now have fully rendered with a ref on the new span.
636+
expect(ref.current).toBe(span);
637+
expect(container.textContent).toBe('Hi');
638+
// If we ended up hydrating the existing content, we won't have properly
639+
// patched up the tree, which might mean we haven't patched the className.
640+
expect(span.className).toBe('hi');
641+
});
642+
563643
it('blocks the update to hydrate first if context has changed', async () => {
564644
let suspend = false;
565645
let resolve;

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ import {
173173
requestCurrentTime,
174174
retryDehydratedSuspenseBoundary,
175175
scheduleWork,
176+
renderDidSuspendDelayIfPossible,
176177
} from './ReactFiberWorkLoop';
177178

178179
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
@@ -2060,6 +2061,8 @@ function updateDehydratedSuspenseComponent(
20602061
// since we now have higher priority work, but in case it doesn't, we need to prepare to
20612062
// render something, if we time out. Even if that requires us to delete everything and
20622063
// skip hydration.
2064+
// Delay having to do this as long as the suspense timeout allows us.
2065+
renderDidSuspendDelayIfPossible();
20632066
return retrySuspenseComponentWithoutHydrating(
20642067
current,
20652068
workInProgress,

0 commit comments

Comments
 (0)