Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tail="collapsed" option to SuspenseList #16007

Merged
merged 2 commits into from
Jul 2, 2019

Conversation

sebmarkbage
Copy link
Collaborator

@sebmarkbage sebmarkbage commented Jun 27, 2019

Builds on top of #16005.

This add an option to SuspenseList which ensures that we avoid inserting any new rows at the tail of the list past some point.

The first option is "collapsed" which means that there is only one row visible at the end of the list. I plan on adding a "hidden" option which ensures that zero visible rows are at the end.

Note that the tail in terms of unfolding row-by-row is defined as any insertions at the bottom or any rows that updated to become suspended or were already inserted from previous commits. This option doesn't actually remove or hide those. However, this is a rare edge cases. Typically you're expected to clear the list for these cases.

The use case here is for streaming rendering of items. This lets you provide more rows to React than you have available yet. The main purpose of this is for server rendering where you can't update to add more. However, it also is useful so that React can prerender later rows while blocked on previous rows. This PR doesn't actually do that yet tho.

This option also allows to render CPU bound work, one item at a time without showing all the fallbacks. Thanks to the tail expiration time.

Collapsing to the last row

The tail="collapsed" option uses the first new row for showing the fallback state. That's fairly efficient because we've rendered it when we tried the last row.

If you want to instead show the last row, you have to use a nested suspense list and the "hidden" option of the inner one:

<SuspenseList revealOrder="forward">
  <SuspenseList revealOrder="forward" tail="hidden">
    ...
  </SuspenseList>
  <Tail />
</SuspenseList>

Tail in this case can be a Suspense boundary that unsuspends when we're not expected to get more items added to the list.

function Blocker({suspendOn}) {
  if (!suspendOn.isResolved()) throw something();
  return null;
}
function Tail() {
  return <Suspense fallback={<Shimmer />}>
    <Blocker suspendOn={endOfTheList} />
  </Suspend>;
}

This technique ensures that if endOfTheList resolves on the server, the SSR streaming can hide the tail shimmer. No need for client rerenders. Placing it in an outer SuspenseList also ensures that once endOfTheList is resolved we don't hide the Shimmer before the rows in the list are fully loaded. Otherwise, the shimmer would hide first and then we'd wait for the inner rows but they're hidden.

This is a bit unfortunate since I think this is a pretty common use case for visualizing a tail load differently from suspended existing boundaries.

In theory we could add an option to collapse into the last row but it gets tricky. Because after the first commit, that row would now be mounted and now it's not a new insertion anymore. That complicates the semantics a bit. The collapsed tail would have to be something like everything after the last committed row that isn't the last one.

This approach doesn’t actually work:

<SuspenseList>
  {rows}
  <Tail />
</SuspenseList>

Because that’s actually just two rows since the fragment in that case is treated as a single row.

I feel like it's just clearer with two lists. That also makes it clearer what a "row" means. E.g. if your tail has multiple rows, it kind of just works.

I also suspect that at some point SuspenseList will accept a custom streaming data type and in that case it might be easier to think of the inner list as one stream and the outer as another.

Although a counter point is that virtualization might make this api difficult/impossible to use.

A possible option would be to add an explicit tail component:

<SuspenseList tailMarker={<Tail />}>
  ...
</SuspenseList>

However, that would require two fibers to be stored similar to how Suspense inserts extra fibers.

@sizebot
Copy link

sizebot commented Jun 27, 2019

ReactDOM: size: 0.0%, gzip: -0.1%

Details of bundled changes.

Comparing: 933c664...ccdb8b0

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.profiling.min.js +0.2% +0.3% 114.71 KB 114.98 KB 36.17 KB 36.26 KB NODE_PROFILING
react-dom-unstable-native-dependencies.development.js 0.0% -0.0% 60.76 KB 60.76 KB 15.85 KB 15.85 KB UMD_DEV
ReactDOM-dev.js +0.3% +0.3% 916.73 KB 919.29 KB 203.46 KB 204.04 KB FB_WWW_DEV
react-dom-unstable-native-dependencies.production.min.js 0.0% -0.1% 10.74 KB 10.74 KB 3.68 KB 3.68 KB UMD_PROD
ReactDOMServer-dev.js 0.0% -0.0% 134.27 KB 134.27 KB 34.45 KB 34.44 KB FB_WWW_DEV
react-dom-unstable-fire.development.js +0.3% +0.3% 893.94 KB 896.43 KB 203.21 KB 203.79 KB UMD_DEV
react-dom-unstable-fire.production.min.js 🔺+0.2% 🔺+0.3% 111 KB 111.26 KB 35.74 KB 35.83 KB UMD_PROD
react-dom-unstable-fire.profiling.min.js +0.2% +0.2% 114.44 KB 114.71 KB 36.79 KB 36.88 KB UMD_PROFILING
react-dom-unstable-fire.development.js +0.3% +0.3% 888.23 KB 890.71 KB 201.69 KB 202.27 KB NODE_DEV
react-dom-server.node.development.js 0.0% 0.0% 133.8 KB 133.8 KB 35.42 KB 35.43 KB NODE_DEV
react-dom-unstable-fire.production.min.js 🔺+0.2% 🔺+0.2% 111.08 KB 111.34 KB 35.25 KB 35.34 KB NODE_PROD
react-dom.development.js +0.3% +0.3% 893.59 KB 896.08 KB 203.06 KB 203.64 KB UMD_DEV
react-dom-unstable-fire.profiling.min.js +0.2% +0.3% 114.72 KB 114.99 KB 36.17 KB 36.27 KB NODE_PROFILING
react-dom.production.min.js 🔺+0.2% 🔺+0.3% 110.98 KB 111.25 KB 35.73 KB 35.82 KB UMD_PROD
ReactFire-dev.js +0.3% +0.3% 915.94 KB 918.5 KB 203.35 KB 203.93 KB FB_WWW_DEV
react-dom-server.browser.production.min.js 0.0% -0.0% 19.34 KB 19.34 KB 7.23 KB 7.23 KB UMD_PROD
react-dom.profiling.min.js +0.2% +0.2% 114.42 KB 114.69 KB 36.78 KB 36.87 KB UMD_PROFILING
ReactFire-prod.js 🔺+0.2% 🔺+0.3% 360.47 KB 361.37 KB 65.76 KB 65.99 KB FB_WWW_PROD
react-dom.development.js +0.3% +0.3% 887.88 KB 890.37 KB 201.55 KB 202.12 KB NODE_DEV
ReactFire-profiling.js +0.2% +0.3% 366.79 KB 367.67 KB 67.1 KB 67.32 KB FB_WWW_PROFILING
react-dom-server.browser.development.js 0.0% 0.0% 131.88 KB 131.88 KB 34.89 KB 34.89 KB NODE_DEV
react-dom.production.min.js 🔺+0.2% 🔺+0.2% 111.06 KB 111.33 KB 35.25 KB 35.33 KB NODE_PROD
ReactDOM-prod.js 🔺+0.2% 🔺+0.3% 372.49 KB 373.38 KB 68.16 KB 68.37 KB FB_WWW_PROD
ReactDOMServer-prod.js 0.0% -0.0% 46.67 KB 46.67 KB 10.74 KB 10.74 KB FB_WWW_PROD
ReactDOM-profiling.js +0.2% +0.3% 378.85 KB 379.72 KB 69.5 KB 69.72 KB FB_WWW_PROFILING
react-dom-unstable-native-dependencies.development.js 0.0% -0.0% 60.43 KB 60.43 KB 15.72 KB 15.72 KB NODE_DEV
react-dom-unstable-fizz.node.development.js 0.0% -0.2% 3.85 KB 3.85 KB 1.5 KB 1.5 KB NODE_DEV
react-dom-unstable-native-dependencies.production.min.js 0.0% -0.1% 10.48 KB 10.48 KB 3.58 KB 3.58 KB NODE_PROD
react-dom-unstable-fizz.node.production.min.js 0.0% -0.3% 1.1 KB 1.1 KB 667 B 665 B NODE_PROD
react-dom-test-utils.development.js 0.0% -0.0% 57.82 KB 57.82 KB 15.89 KB 15.89 KB UMD_DEV
react-dom-unstable-fizz.browser.development.js 0.0% -0.1% 3.78 KB 3.78 KB 1.52 KB 1.52 KB UMD_DEV
react-dom-test-utils.production.min.js 0.0% -0.0% 10.95 KB 10.95 KB 4.01 KB 4.01 KB UMD_PROD
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.3% 1.21 KB 1.21 KB 706 B 704 B UMD_PROD
react-dom-test-utils.development.js 0.0% -0.0% 56.08 KB 56.08 KB 15.55 KB 15.55 KB NODE_DEV
react-dom-unstable-fizz.browser.development.js 0.0% -0.1% 3.61 KB 3.61 KB 1.48 KB 1.47 KB NODE_DEV
react-dom-test-utils.production.min.js 0.0% -0.0% 10.71 KB 10.71 KB 3.95 KB 3.94 KB NODE_PROD
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.3% 1.05 KB 1.05 KB 637 B 635 B NODE_PROD

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactART-dev.js +0.4% +0.5% 569.45 KB 572.01 KB 119.43 KB 120.02 KB FB_WWW_DEV
react-art.development.js +0.4% +0.4% 625.55 KB 628.04 KB 137.26 KB 137.85 KB UMD_DEV
react-art.production.min.js 🔺+0.3% 🔺+0.3% 101.97 KB 102.23 KB 31.18 KB 31.28 KB UMD_PROD
react-art.development.js +0.4% +0.5% 556.41 KB 558.9 KB 119.89 KB 120.47 KB NODE_DEV
react-art.production.min.js 🔺+0.4% 🔺+0.4% 67.02 KB 67.29 KB 20.53 KB 20.62 KB NODE_PROD
ReactART-prod.js 🔺+0.4% 🔺+0.6% 217.8 KB 218.69 KB 36.91 KB 37.13 KB FB_WWW_PROD

react-test-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-test-renderer.development.js +0.4% +0.5% 568.61 KB 571.1 KB 122.54 KB 123.11 KB UMD_DEV
react-test-renderer.production.min.js 🔺+0.4% 🔺+0.5% 68.36 KB 68.63 KB 21.01 KB 21.1 KB UMD_PROD
react-test-renderer.development.js +0.4% +0.5% 564.15 KB 566.64 KB 121.37 KB 121.97 KB NODE_DEV
react-test-renderer.production.min.js 🔺+0.4% 🔺+0.4% 68.09 KB 68.36 KB 20.74 KB 20.83 KB NODE_PROD
ReactTestRenderer-dev.js +0.4% +0.5% 579.23 KB 581.79 KB 121.7 KB 122.33 KB FB_WWW_DEV
react-test-renderer-shallow.development.js 0.0% -0.0% 41.46 KB 41.46 KB 10.79 KB 10.78 KB UMD_DEV
react-test-renderer-shallow.production.min.js 0.0% -0.1% 11.66 KB 11.66 KB 3.56 KB 3.56 KB UMD_PROD
react-test-renderer-shallow.development.js 0.0% -0.0% 35.6 KB 35.6 KB 9.42 KB 9.41 KB NODE_DEV
react-test-renderer-shallow.production.min.js 0.0% -0.1% 11.8 KB 11.8 KB 3.68 KB 3.68 KB NODE_PROD

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactFabric-dev.js +0.4% +0.4% 719.03 KB 721.58 KB 152.77 KB 153.37 KB RN_FB_DEV
ReactFabric-prod.js 🔺+0.3% 🔺+0.5% 265.31 KB 266.2 KB 45.71 KB 45.93 KB RN_FB_PROD
ReactNativeRenderer-dev.js +0.4% +0.4% 703.87 KB 706.43 KB 150 KB 150.59 KB RN_OSS_DEV
ReactFabric-profiling.js +0.3% +0.4% 273.33 KB 274.2 KB 47.17 KB 47.35 KB RN_FB_PROFILING
ReactNativeRenderer-prod.js 🔺+0.3% 🔺+0.5% 271.21 KB 272.1 KB 46.56 KB 46.78 KB RN_OSS_PROD
ReactNativeRenderer-profiling.js +0.3% +0.4% 278.64 KB 279.52 KB 48.12 KB 48.3 KB RN_OSS_PROFILING
ReactNativeRenderer-dev.js +0.4% +0.4% 703.96 KB 706.52 KB 150.06 KB 150.64 KB RN_FB_DEV
ReactNativeRenderer-prod.js 🔺+0.3% 🔺+0.5% 271.2 KB 272.09 KB 46.57 KB 46.79 KB RN_FB_PROD
ReactNativeRenderer-profiling.js +0.3% +0.4% 278.63 KB 279.51 KB 48.12 KB 48.31 KB RN_FB_PROFILING
ReactFabric-dev.js +0.4% +0.4% 718.92 KB 721.48 KB 152.73 KB 153.32 KB RN_OSS_DEV
ReactFabric-prod.js 🔺+0.3% 🔺+0.5% 265.3 KB 266.19 KB 45.7 KB 45.92 KB RN_OSS_PROD
ReactFabric-profiling.js +0.3% +0.4% 273.33 KB 274.21 KB 47.16 KB 47.34 KB RN_OSS_PROFILING

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler-reflection.development.js 0.0% -0.0% 18.66 KB 18.66 KB 6.03 KB 6.03 KB NODE_DEV
react-reconciler-reflection.production.min.js 0.0% -0.1% 2.58 KB 2.58 KB 1.13 KB 1.13 KB NODE_PROD
react-reconciler-persistent.development.js +0.5% +0.5% 552.29 KB 554.78 KB 117.39 KB 117.96 KB NODE_DEV
react-reconciler-persistent.production.min.js 🔺+0.4% 🔺+0.4% 68.27 KB 68.54 KB 20.34 KB 20.43 KB NODE_PROD
react-reconciler.development.js +0.4% +0.5% 554.7 KB 557.19 KB 118.44 KB 119.02 KB NODE_DEV
react-reconciler.production.min.js 🔺+0.4% 🔺+0.4% 68.26 KB 68.53 KB 20.34 KB 20.43 KB NODE_PROD

Generated by 🚫 dangerJS

@bvaughn
Copy link
Contributor

bvaughn commented Jun 27, 2019

A possible option would be to add an explicit tail component:

<SuspenseList tailMarker={<Tail />}>
 ...
</SuspenseList>

However, that would require two fibers to be stored similar to how Suspense inserts extra fibers.

This API was my initial intuition as well. I'm not sure I understand the statement about requiring two fibers. Wouldn't nested lists also require two fibers?

Copy link
Contributor

@bvaughn bvaughn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only reviewed the delta between the two branches and I don't have as much context on this feature anyway, so take my feedback with a grain of salt.

'Did you mean "collapsed"?',
tailOptions,
);
} else if (revealOrder !== 'forwards' && revealOrder !== 'backwards') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why else if?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So that you don't get both warnings at once. Seems more actionable to pick a legit value first. E.g. if you specified something like "none", it would be misleading to recommend a revealOrder.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. I guess that makes sense. I was worried it could feel a bit like warning-whack-a-mole if you fixed one just to see another pop up.

The revealOrder would still be invalid if you specified tail="none" though so wouldn't we still want to warn about it?

I dunno. I don't feel strongly here 😄 Was just asking.

break;
}
default: {
// TODO: warn if not undefined
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You already warned in begin.

@@ -24,6 +26,8 @@ export type SuspenseListState = {|
tail: null | Fiber,
// The absolute time in ms that we'll expire the tail rendering.
tailExpiration: number,
// Tail insertions setting.
tailOptions: SuspenseListTailOptions,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trivial naming nit: I think of "options" as being a config object. This strikes me as more of a "mode" or a "type"

jest.advanceTimersByTime(500);

// Even though B is unsuspended, it's still in loading state because
// it is blocked by C.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems counter-intuitive to me that B is blocked by C in this case. Since we display both loading states, I would also have expected them to resolve in order.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't new behavior in this PR and this is basically just replicating the same assertion as other tests. The rationale for this behavior is that in a variable size list you don't want to pop in things multiple time to shift the below content around multiple times. Therefore the "head" resolves in the "together" mode.

This is kind of a rare case though since insertions in the middle are rare. Let alone multiple of them. A more common variant is inserting at the top but in that case you don't want to start with the top most and then expand them one by one to shift things down. More likely it's better to just show them all as a unit.

<span>C</span>
</Fragment>,
);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth adding some tests that resolve things out of order?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of the other ones resolve out of order, but I don't really want to add every combination. That's more for a fuzz tester.

Implementation detail wise there's also not much to test because we don't even render the future items so we have not attached any listeners.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. Implementation details can always change though 😁

Copy link
Collaborator

@acdlite acdlite left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test cases are so good. Nice job.

Maybe we can also add an explicit tail prop (tailMarker) as an option later? That was also my initial inclination.

We used to assume that this didn't suspend but this branch happens in
both cases. This fixes it so that we first check if we suspended.

Now we can fix the tail so that it always render an additional fallback
in this scenario.
@sebmarkbage
Copy link
Collaborator Author

Found and fixed one more bug. ccdb8b0

@sebmarkbage sebmarkbage merged commit 5cb8f6f into facebook:master Jul 2, 2019
trueadm pushed a commit to trueadm/react that referenced this pull request Jul 3, 2019
* Add tail="collapsed" option

* Fix issue with tail exceeding the CPU time limit

We used to assume that this didn't suspend but this branch happens in
both cases. This fixes it so that we first check if we suspended.

Now we can fix the tail so that it always render an additional fallback
in this scenario.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants