Skip to content

Commit e4b74db

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 a41cf09 commit e4b74db

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
@@ -562,6 +562,86 @@ describe('ReactDOMServerPartialHydration', () => {
562562
expect(container.textContent).toBe('Hi Hi');
563563
});
564564

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