Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Demo: Coordinating async React with non-React views

Demo: Coordinating async React with non-React views

tl;dr I built a demo illustrating what it might look like to add async rendering to Facebook's commenting interface, while ensuring it appears on the screen simultaneous to the server-rendered story.

A key benefit of async rendering is that large updates don't block the main thread; instead, the work is spread out and performed during idle periods using cooperative scheduling.

But once you make something async, you introduce the possibility that things may appear on the screen at separate times. Especially when you're dealing with multiple UI frameworks, as is often the case at Facebook.

How do we solve this with React?

You can think of React rendering as being split into two main phases:

  • the render phase, where we compare the new tree with the existing tree and calculate the changes that need to be made. This phase is free of side-effects, so it can be done asynchronously. The work can also be interrupted, rebased, restarted, and interleaved with other updates.
  • the commit phase, where the changes computed during the render phase are flushed to the screen. This phase is typically very fast and is performed synchronously to avoid inconsistencies.

Because these two phases are separate, we can expose an API to the developer to control exactly when the commit phase is executed. Before then, the async work can be completed, but nothing is actually updated on the screen. That way we can coordinate React's commit phase with non-React renderers.

The new API will look like this:

const root = ReactDOM.unstable_createRoot(containerEl);
const work = root.prerender(<App />);
// ... other async work ...
work.commit();

You can also await work to ensure that the render phase is complete. (If you don't, the remaining work is flushed synchronously, which may or may not be what you want.)

const work = root.prerender(<App />);
await work;
work.commit();

Another neat feature is the ability to start rendering before the DOM container is even available:

let containerEl;
const root = ReactDOM.unstable_createLazyRoot(function getContainer() {
 return containerEl;
});
const work = root.prerender(<App />);
containerEl = await promiseForContainer;
work.commit();

Play around with the demo and see for yourself! Hopefully this helps illustrate how async rendering in React can be used to build products.

We'll share more demos and examples in the future.

@cmmartin

This comment has been minimized.

Show comment Hide comment
@cmmartin

cmmartin Sep 19, 2017

If you don't actually need the container until the commit phase, why not simplify the API by moving prerender to the top level and making commit accept the container? Then, the last example looks like this...

ReactDOM.unstable_prerender(<App />).commit(await promiseForContainer);

And awaiting work or other async work

const work = ReactDOM.unstable_prerender(<App />);
// ... other async work ...
// await work;
work.commit(containerEl);

Now, we have 2 public functions

unstable_prerender, commit

Instead of 4

unstable_createRoot, unstable_createLazyRoot, prerender, commit

cmmartin commented Sep 19, 2017

If you don't actually need the container until the commit phase, why not simplify the API by moving prerender to the top level and making commit accept the container? Then, the last example looks like this...

ReactDOM.unstable_prerender(<App />).commit(await promiseForContainer);

And awaiting work or other async work

const work = ReactDOM.unstable_prerender(<App />);
// ... other async work ...
// await work;
work.commit(containerEl);

Now, we have 2 public functions

unstable_prerender, commit

Instead of 4

unstable_createRoot, unstable_createLazyRoot, prerender, commit

@acdlite

This comment has been minimized.

Show comment Hide comment
@acdlite

acdlite Sep 19, 2017

@cmmartin Good question!

Consider that we need to support updates, too:

const root = ReactDOM.unstable_createRoot(containerEl);

// Initial mount
const mount = root.prerender(<App foo="A" />);
await mount;
mount.commit();

// Update
const update = root.prerender(<App foo="B" />);
await update;
update.commit();

// Or, as a shortcut, if you don't need to coordinate commit
root.render(<App foo="C" />);

Now, you could do this without the root object by always passing the container to prerender and render. In which case, React will secretly attach the root to the container. That's how it works in the current API:

// Initial mount. Root is secretly attached to `containerEl._reactRootContainer`
ReactDOM.render(<App foo="A" />, containerEl);
// Update
ReactDOM.render(<App foo="B" />, containerEl);

A few problems with this API. One, you have to pass the container every time.

The more interesting case is lazy containers. If you don't have a DOM container already, what do you use as the "root"? What do you pass as the second parameter to ReactDOM.render? How do you update something that hasn't yet resolved?

That's why we think exposing the root to the developer is better.

let containerEl;
const root = ReactDOM.unstable_createLazyRoot(() => containerEl);
const mount = root.prerender(<App foo="A" />);
const update = root.prerender(<App foo="B" />);
containerEl = await promiseForContainer;
update.commit();

Plus, it means we don't have to attach secret junk to the DOM element, which is nice.

Another thing to consider is hydration of server-rendered components. When you mount a tree into a DOM container, React needs to know whether to clear out any existing children in the container. By default, it does. But in the case of hydration, it should preserve the server-rendered content and render on top of it.

We used to try to infer hydration mode automatically, but we recently switched to an explicit API:

ReactDOM.hydrate(<App />, containerEl);

But again, what about updates? What if someone does this?

// Mount, hydration is on
ReactDOM.hydrate(<App />, containerEl);
// Update, hydration is off. If the mount is async and hasn't finished yet, it will clear the existing children!
ReactDOM.render(<App />, containerEl);

It turns out that if you want to use hydration the first time on the root, you should also use hydration on all subsequent updates. We can express this using the root API:

const root = ReactDOM.unstable_createRoot(containerEl, { hydrate: true });
// Hydration is on
root.render(<App />);
// even for updates
root.render(<App />);

This API also lets us disallow hydration for lazy roots, since it's not possible to hydrate a container that hasn't resolved yet.

Hope this explanation helps! These APIs are still under consideration, so if you have better suggestions that meet the constraints I've described here, please let me know!

(Also if you comment, you might want to ping me on twitter at acdlite to make sure I see it.)

Owner

acdlite commented Sep 19, 2017

@cmmartin Good question!

Consider that we need to support updates, too:

const root = ReactDOM.unstable_createRoot(containerEl);

// Initial mount
const mount = root.prerender(<App foo="A" />);
await mount;
mount.commit();

// Update
const update = root.prerender(<App foo="B" />);
await update;
update.commit();

// Or, as a shortcut, if you don't need to coordinate commit
root.render(<App foo="C" />);

Now, you could do this without the root object by always passing the container to prerender and render. In which case, React will secretly attach the root to the container. That's how it works in the current API:

// Initial mount. Root is secretly attached to `containerEl._reactRootContainer`
ReactDOM.render(<App foo="A" />, containerEl);
// Update
ReactDOM.render(<App foo="B" />, containerEl);

A few problems with this API. One, you have to pass the container every time.

The more interesting case is lazy containers. If you don't have a DOM container already, what do you use as the "root"? What do you pass as the second parameter to ReactDOM.render? How do you update something that hasn't yet resolved?

That's why we think exposing the root to the developer is better.

let containerEl;
const root = ReactDOM.unstable_createLazyRoot(() => containerEl);
const mount = root.prerender(<App foo="A" />);
const update = root.prerender(<App foo="B" />);
containerEl = await promiseForContainer;
update.commit();

Plus, it means we don't have to attach secret junk to the DOM element, which is nice.

Another thing to consider is hydration of server-rendered components. When you mount a tree into a DOM container, React needs to know whether to clear out any existing children in the container. By default, it does. But in the case of hydration, it should preserve the server-rendered content and render on top of it.

We used to try to infer hydration mode automatically, but we recently switched to an explicit API:

ReactDOM.hydrate(<App />, containerEl);

But again, what about updates? What if someone does this?

// Mount, hydration is on
ReactDOM.hydrate(<App />, containerEl);
// Update, hydration is off. If the mount is async and hasn't finished yet, it will clear the existing children!
ReactDOM.render(<App />, containerEl);

It turns out that if you want to use hydration the first time on the root, you should also use hydration on all subsequent updates. We can express this using the root API:

const root = ReactDOM.unstable_createRoot(containerEl, { hydrate: true });
// Hydration is on
root.render(<App />);
// even for updates
root.render(<App />);

This API also lets us disallow hydration for lazy roots, since it's not possible to hydrate a container that hasn't resolved yet.

Hope this explanation helps! These APIs are still under consideration, so if you have better suggestions that meet the constraints I've described here, please let me know!

(Also if you comment, you might want to ping me on twitter at acdlite to make sure I see it.)

@NE-SmallTown

This comment has been minimized.

Show comment Hide comment
@NE-SmallTown

NE-SmallTown Sep 26, 2017

@acdlite Hi, can you reply some issues under your react-fiber-architecture repo? Thanks! :)

NE-SmallTown commented Sep 26, 2017

@acdlite Hi, can you reply some issues under your react-fiber-architecture repo? Thanks! :)

@acdlite

This comment has been minimized.

Show comment Hide comment
@acdlite

acdlite Sep 26, 2017

@NE-SmallTown Hey, I'll try to get to those issues as soon as I can. That repo is likely headed for a complete rewrite, with content focused a bit more on use cases and examples as in this Gist. Thanks for your patience!

Owner

acdlite commented Sep 26, 2017

@NE-SmallTown Hey, I'll try to get to those issues as soon as I can. That repo is likely headed for a complete rewrite, with content focused a bit more on use cases and examples as in this Gist. Thanks for your patience!

@NE-SmallTown

This comment has been minimized.

Show comment Hide comment
@NE-SmallTown

NE-SmallTown Sep 28, 2017

@acdlite Happy to hear about that, thanks for your great work :)

NE-SmallTown commented Sep 28, 2017

@acdlite Happy to hear about that, thanks for your great work :)

@xgqfrms-GitHub

This comment has been minimized.

Show comment Hide comment

xgqfrms-GitHub commented Oct 8, 2017

@funwithtriangles

This comment has been minimized.

Show comment Hide comment
@funwithtriangles

funwithtriangles Nov 25, 2017

When can we expect to see this in production? Looks like it would fix some perf issues I'm having.

When can we expect to see this in production? Looks like it would fix some perf issues I'm having.

@lixiaoyan

This comment has been minimized.

Show comment Hide comment
@lixiaoyan

lixiaoyan Jan 26, 2018

I found another thing called "React.unstable_AsyncComponent" in the source code. Could you explain what it is and why we need it?

I found another thing called "React.unstable_AsyncComponent" in the source code. Could you explain what it is and why we need it?

@sw-yx

This comment has been minimized.

Show comment Hide comment
@sw-yx

sw-yx Mar 31, 2018

lagradar2

just wanted to share how this demo looks together with the lag radar from dan abramov's talk.. pretty clear when the red lag shows up!

sw-yx commented Mar 31, 2018

lagradar2

just wanted to share how this demo looks together with the lag radar from dan abramov's talk.. pretty clear when the red lag shows up!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment