Skip to content

Commit

Permalink
Partial Hydration (#14717)
Browse files Browse the repository at this point in the history
* Basic partial hydration test

* Render comments around Suspense components

We need this to be able to identify how far to skip ahead if we're not
going to hydrate this subtree yet.

* Add DehydratedSuspenseComponent type of work

Will be used for Suspense boundaries that are left with their server
rendered content intact.

* Add comment node as hydratable instance type as placeholder for suspense

* Skip past nodes within the Suspense boundary

This lets us continue hydrating sibling nodes.

* A dehydrated suspense boundary comment should be considered a sibling

* Retry hydrating at offscreen pri or after ping if suspended

* Enter hydration state when retrying dehydrated suspense boundary

* Delete all children within a dehydrated suspense boundary when it's deleted

* Delete server rendered content when props change before hydration completes

* Make test internal

* Wrap in act

* Change SSR Fixture to use Partial Hydration

This requires the enableSuspenseServerRenderer flag to be manually enabled
for the build to work.

* Changes to any parent Context forces clearing dehydrated content

We mark dehydrated boundaries as having child work, since they might have
components that read from the changed context.

We check this in beginWork and if it does we treat it as if the input
has changed (same as if props changes).

* Wrap in feature flag

* Treat Suspense boundaries without fallbacks as if not-boundaries

These don't come into play for purposes of hydration.

* Fix clearing of nested suspense boundaries

* ping -> retry

Co-Authored-By: sebmarkbage <sebastian@calyptus.eu>

* Typo

Co-Authored-By: sebmarkbage <sebastian@calyptus.eu>

* Use didReceiveUpdate instead of manually comparing props

* Leave comment for why it's ok to ignore the timeout
  • Loading branch information
sebmarkbage authored Feb 12, 2019
1 parent f24a0da commit f3a1495
Show file tree
Hide file tree
Showing 24 changed files with 1,417 additions and 130 deletions.
39 changes: 27 additions & 12 deletions fixtures/ssr/src/components/App.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
import React, {Component} from 'react';
import React, {useContext, useState, Suspense} from 'react';

import Chrome from './Chrome';
import Page from './Page';
import Page2 from './Page2';
import Theme from './Theme';

export default class App extends Component {
render() {
return (
<Chrome title="Hello World" assets={this.props.assets}>
<div>
<h1>Hello World</h1>
<Page />
</div>
</Chrome>
);
}
function LoadingIndicator() {
let theme = useContext(Theme);
return <div className={theme + '-loading'}>Loading...</div>;
}

export default function App({assets}) {
let [CurrentPage, switchPage] = useState(() => Page);
return (
<Chrome title="Hello World" assets={assets}>
<div>
<h1>Hello World</h1>
<a className="link" onClick={() => switchPage(() => Page)}>
Page 1
</a>
{' | '}
<a className="link" onClick={() => switchPage(() => Page2)}>
Page 2
</a>
<Suspense fallback={<LoadingIndicator />}>
<CurrentPage />
</Suspense>
</div>
</Chrome>
);
}
24 changes: 24 additions & 0 deletions fixtures/ssr/src/components/Chrome.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,27 @@ body {
padding: 0;
font-family: sans-serif;
}

body.light {
background-color: #FFFFFF;
color: #333333;
}

body.dark {
background-color: #000000;
color: #CCCCCC;
}

.light-loading {
margin: 10px 0;
padding: 10px;
background-color: #CCCCCC;
color: #666666;
}

.dark-loading {
margin: 10px 0;
padding: 10px;
background-color: #333333;
color: #999999;
}
12 changes: 10 additions & 2 deletions fixtures/ssr/src/components/Chrome.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import React, {Component} from 'react';

import Theme, {ThemeToggleButton} from './Theme';

import './Chrome.css';

export default class Chrome extends Component {
state = {theme: 'light'};
render() {
const assets = this.props.assets;
return (
Expand All @@ -14,13 +17,18 @@ export default class Chrome extends Component {
<link rel="stylesheet" href={assets['main.css']} />
<title>{this.props.title}</title>
</head>
<body>
<body className={this.state.theme}>
<noscript
dangerouslySetInnerHTML={{
__html: `<b>Enable JavaScript to run this app.</b>`,
}}
/>
{this.props.children}
<Theme.Provider value={this.state.theme}>
{this.props.children}
<div>
<ThemeToggleButton onChange={theme => this.setState({theme})} />
</div>
</Theme.Provider>
<script
dangerouslySetInnerHTML={{
__html: `assetManifest = ${JSON.stringify(assets)};`,
Expand Down
15 changes: 14 additions & 1 deletion fixtures/ssr/src/components/Page.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
.bold {
.link {
font-weight: bold;
cursor: pointer;
}
.light-box {
margin: 10px 0;
padding: 10px;
background-color: #CCCCCC;
color: #333333;
}
.dark-box {
margin: 10px 0;
padding: 10px;
background-color: #333333;
color: #CCCCCC;
}
20 changes: 14 additions & 6 deletions fixtures/ssr/src/components/Page.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import React, {Component} from 'react';

import Theme from './Theme';
import Suspend from './Suspend';

import './Page.css';

const autofocusedInputs = [
Expand All @@ -14,17 +17,22 @@ export default class Page extends Component {
};
render() {
const link = (
<a className="bold" onClick={this.handleClick}>
<a className="link" onClick={this.handleClick}>
Click Here
</a>
);
return (
<div>
<p suppressHydrationWarning={true}>A random number: {Math.random()}</p>
<p>Autofocus on page load: {autofocusedInputs}</p>
<p>{!this.state.active ? link : 'Thanks!'}</p>
{this.state.active && <p>Autofocus on update: {autofocusedInputs}</p>}
<div className={this.context + '-box'}>
<Suspend>
<p suppressHydrationWarning={true}>
A random number: {Math.random()}
</p>
<p>Autofocus on page load: {autofocusedInputs}</p>
<p>{!this.state.active ? link : 'Thanks!'}</p>
{this.state.active && <p>Autofocus on update: {autofocusedInputs}</p>}
</Suspend>
</div>
);
}
}
Page.contextType = Theme;
15 changes: 15 additions & 0 deletions fixtures/ssr/src/components/Page2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React, {useContext} from 'react';

import Theme from './Theme';
import Suspend from './Suspend';

import './Page.css';

export default function Page2() {
let theme = useContext(Theme);
return (
<div className={theme + '-box'}>
<Suspend>Content of a different page</Suspend>
</div>
);
}
21 changes: 21 additions & 0 deletions fixtures/ssr/src/components/Suspend.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
let promise = null;
let isResolved = false;

export default function Suspend({children}) {
// This will suspend the content from rendering but only on the client.
// This is used to demo a slow loading app.
if (typeof window === 'object') {
if (!isResolved) {
if (promise === null) {
promise = new Promise(resolve => {
setTimeout(() => {
isResolved = true;
resolve();
}, 6000);
});
}
throw promise;
}
}
return children;
}
25 changes: 25 additions & 0 deletions fixtures/ssr/src/components/Theme.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React, {createContext, useContext, useState} from 'react';

const Theme = createContext('light');

export default Theme;

export function ThemeToggleButton({onChange}) {
let theme = useContext(Theme);
let [targetTheme, setTargetTheme] = useState(theme);
function toggleTheme() {
let newTheme = theme === 'light' ? 'dark' : 'light';
// High pri, responsive update.
setTargetTheme(newTheme);
// Perform the actual theme change in a separate update.
setTimeout(() => onChange(newTheme), 0);
}
if (targetTheme !== theme) {
return 'Switching to ' + targetTheme + '...';
}
return (
<a className="link" onClick={toggleTheme}>
Switch to {theme === 'light' ? 'Dark' : 'Light'} theme
</a>
);
}
5 changes: 3 additions & 2 deletions fixtures/ssr/src/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import {hydrate} from 'react-dom';
import {unstable_createRoot} from 'react-dom';

import App from './components/App';

hydrate(<App assets={window.assetManifest} />, document);
let root = unstable_createRoot(document, {hydrate: true});
root.render(<App assets={window.assetManifest} />);
Loading

0 comments on commit f3a1495

Please sign in to comment.