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

Rewrite useTransition to better handle overlapping transitions #17908

Closed
wants to merge 3 commits into from

Conversation

acdlite
Copy link
Collaborator

@acdlite acdlite commented Jan 24, 2020

When the user interacts with the UI more quickly than it can respond, you usually don't want to update the screen with anything that isn't the last thing the user requested.

For example, if you click on multiple tabs in, we should not switch to any tab that isn't the last one you clicked. The last tab navigation supersedes all the previous ones.

However, this only applies to transitions that update the same part of the UI. For example, if you click a new tab, and in the meantime you click a drop-down menu, it doesn't matter whether the tab or the menu appears first. They are independent, parallel transitions.

To implement this behavior, we will treat transitions as overlapping if they share at least one state queue between them. The result of the most recent transition in an overlapping sequence is considered the terminal state. The rest are considered intermediate states. We will avoid showing intermediate states by batching them together with the terminal one.

When there are overlapping transitions, the isPending states of the useTransition hooks that correspond to the intermediate states are turned off (unless they happen to be the same useTransition hook as the terminal one).


I iterated on the implementation several times. I've squashed most of the commits to reduce noise when reviewing. You can see some of the unsquashed steps in #17418. If this is too much to review in a single PR, I can split the commits back up again; however, I don't think it's worth landing in more than two steps because there are only two useful features here. Increasing the granularity would increase the risk of landing without much benefit.

Example (demo)

The example demo is a tab switcher. Pay attention to when the pending spinner disappears, and when the tab content is allowed to switch.

Before this PR: https://codesandbox.io/s/elastic-hawking-69381
After this PR: https://codesandbox.io/s/beautiful-goldstine-u157u

@facebook-github-bot facebook-github-bot added React Core Team Opened by a member of the React Core Team CLA Signed labels Jan 24, 2020
@acdlite acdlite mentioned this pull request Jan 24, 2020
2 tasks
@codesandbox-ci
Copy link

codesandbox-ci bot commented Jan 24, 2020

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit fe9d199:

Sandbox Source
blazing-wildflower-3obs1 Configuration
strange-carson-2yowi PR
intelligent-dewdney-kt5c9 PR

@sizebot
Copy link

sizebot commented Jan 24, 2020

Details of bundled changes.

Comparing: cf00812...fe9d199

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-art.production.min.js 🔺+1.6% 🔺+2.0% 104.77 KB 106.42 KB 31.83 KB 32.46 KB UMD_PROD
react-art.development.js +1.7% +1.5% 678.09 KB 689.73 KB 145.94 KB 148.07 KB UMD_DEV
react-art.development.js +1.9% +1.7% 608.77 KB 620.41 KB 128.52 KB 130.65 KB NODE_DEV
react-art.production.min.js 🔺+2.4% 🔺+2.7% 69.77 KB 71.42 KB 21 KB 21.56 KB NODE_PROD

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler-persistent.production.min.js 🔺+2.3% 🔺+2.8% 72.52 KB 74.17 KB 21.31 KB 21.91 KB NODE_PROD
react-reconciler.development.js +1.9% +1.7% 609.78 KB 621.42 KB 127.69 KB 129.86 KB NODE_DEV
react-reconciler.production.min.js 🔺+2.3% 🔺+2.8% 72.51 KB 74.16 KB 21.31 KB 21.9 KB NODE_PROD
react-reconciler-reflection.development.js 0.0% -0.0% 19.51 KB 19.51 KB 6.39 KB 6.39 KB NODE_DEV
react-reconciler-persistent.development.js +1.9% +1.7% 606.97 KB 618.61 KB 126.5 KB 128.69 KB NODE_DEV

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactFabric-dev.js +1.6% +1.4% 741.53 KB 753.3 KB 155.7 KB 157.8 KB RN_OSS_DEV
ReactFabric-prod.js 🔺+2.2% 🔺+1.5% 266.8 KB 272.6 KB 45.77 KB 46.43 KB RN_OSS_PROD
ReactNativeRenderer-dev.js +1.6% +1.3% 750.91 KB 762.68 KB 157.91 KB 160.02 KB RN_OSS_DEV
ReactFabric-profiling.js +2.1% +1.4% 277.96 KB 283.69 KB 47.89 KB 48.54 KB RN_OSS_PROFILING
ReactNativeRenderer-prod.js 🔺+2.1% 🔺+1.4% 274.37 KB 280.18 KB 47.02 KB 47.67 KB RN_OSS_PROD
ReactNativeRenderer-profiling.js +2.0% +1.3% 285.58 KB 291.31 KB 49.17 KB 49.82 KB RN_OSS_PROFILING

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom-test-utils.development.js 0.0% 0.0% 53.05 KB 53.05 KB 15.08 KB 15.08 KB NODE_DEV
react-dom.production.min.js 🔺+1.4% 🔺+1.8% 115.84 KB 117.5 KB 37.26 KB 37.92 KB UMD_PROD
react-dom-unstable-native-dependencies.development.js 0.0% -0.0% 60.59 KB 60.59 KB 15.99 KB 15.99 KB NODE_DEV
react-dom.profiling.min.js +1.4% +1.8% 119.46 KB 121.12 KB 38.41 KB 39.09 KB UMD_PROFILING
react-dom.development.js +1.2% +1.0% 952.36 KB 963.99 KB 213.54 KB 215.69 KB NODE_DEV
react-dom.production.min.js 🔺+1.4% 🔺+1.7% 115.91 KB 117.57 KB 36.61 KB 37.24 KB NODE_PROD
react-dom.profiling.min.js +1.4% +1.7% 119.68 KB 121.33 KB 37.72 KB 38.36 KB NODE_PROFILING
react-dom-unstable-fizz.browser.development.js 0.0% -0.1% 3.87 KB 3.87 KB 1.54 KB 1.54 KB UMD_DEV
react-dom-server.browser.development.js 0.0% -0.0% 138.88 KB 138.88 KB 36.78 KB 36.78 KB UMD_DEV
react-dom-unstable-fizz.browser.development.js 0.0% -0.1% 3.7 KB 3.7 KB 1.5 KB 1.49 KB NODE_DEV
react-dom-test-utils.development.js 0.0% -0.0% 54.78 KB 54.78 KB 15.4 KB 15.4 KB UMD_DEV
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.2% 1.04 KB 1.04 KB 634 B 633 B NODE_PROD
react-dom-test-utils.production.min.js 0.0% 0.0% 11.17 KB 11.17 KB 4.14 KB 4.14 KB UMD_PROD
react-dom-server.browser.development.js 0.0% 0.0% 134.81 KB 134.81 KB 35.76 KB 35.76 KB NODE_DEV
react-dom-test-utils.production.min.js 0.0% 0.0% 10.94 KB 10.94 KB 4.08 KB 4.08 KB NODE_PROD
react-dom-unstable-fizz.node.development.js 0.0% -0.1% 4.4 KB 4.4 KB 1.64 KB 1.64 KB NODE_DEV
react-dom-unstable-native-dependencies.development.js 0.0% -0.0% 60.89 KB 60.89 KB 16.06 KB 16.06 KB UMD_DEV
react-dom-server.node.development.js 0.0% 0.0% 135.92 KB 135.92 KB 35.99 KB 35.99 KB NODE_DEV
react-dom.development.js +1.2% +1.0% 958.29 KB 969.93 KB 215.22 KB 217.38 KB UMD_DEV

react-test-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-test-renderer.development.js +1.9% +1.6% 622.86 KB 634.5 KB 131.47 KB 133.58 KB UMD_DEV
react-test-renderer.production.min.js 🔺+2.3% 🔺+2.9% 71.91 KB 73.58 KB 21.92 KB 22.56 KB UMD_PROD
react-test-renderer.development.js +1.9% +1.6% 618.13 KB 629.76 KB 130.29 KB 132.41 KB NODE_DEV
react-test-renderer.production.min.js 🔺+2.3% 🔺+2.8% 71.61 KB 73.26 KB 21.54 KB 22.14 KB NODE_PROD

ReactDOM: size: 🔺+1.4%, gzip: 🔺+1.8%

Size changes (stable)

Generated by 🚫 dangerJS against fe9d199

@sizebot
Copy link

sizebot commented Jan 24, 2020

Details of bundled changes.

Comparing: cf00812...fe9d199

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.profiling.min.js +1.5% +1.9% 123.72 KB 125.61 KB 38.78 KB 39.53 KB NODE_PROFILING
react-dom-server.browser.development.js 0.0% 0.0% 138.9 KB 138.9 KB 36.78 KB 36.78 KB UMD_DEV
react-dom-test-utils.development.js 0.0% -0.0% 54.79 KB 54.79 KB 15.4 KB 15.4 KB UMD_DEV
react-dom-unstable-fizz.browser.development.js 0.0% -0.1% 3.88 KB 3.88 KB 1.55 KB 1.55 KB UMD_DEV
react-dom-test-utils.production.min.js 0.0% 0.0% 11.18 KB 11.18 KB 4.15 KB 4.15 KB UMD_PROD
react-dom-test-utils.development.js 0.0% 0.0% 53.06 KB 53.06 KB 15.08 KB 15.08 KB NODE_DEV
react-dom-unstable-fizz.browser.development.js 0.0% -0.1% 3.71 KB 3.71 KB 1.5 KB 1.5 KB NODE_DEV
react-dom-test-utils.production.min.js 0.0% 0.0% 10.95 KB 10.95 KB 4.09 KB 4.09 KB NODE_PROD
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.2% 1.05 KB 1.05 KB 642 B 641 B NODE_PROD
react-dom.development.js +1.2% +1.0% 958.31 KB 969.95 KB 215.24 KB 217.4 KB UMD_DEV
react-dom.production.min.js 🔺+1.6% 🔺+2.0% 119.74 KB 121.64 KB 38.38 KB 39.14 KB UMD_PROD
react-dom.profiling.min.js +1.5% +2.0% 123.47 KB 125.37 KB 39.56 KB 40.35 KB UMD_PROFILING
react-dom.development.js +1.2% +1.0% 952.38 KB 964.01 KB 213.56 KB 215.71 KB NODE_DEV
react-dom-server.node.development.js 0.0% 0.0% 135.95 KB 135.95 KB 35.99 KB 35.99 KB NODE_DEV
react-dom.production.min.js 🔺+1.6% 🔺+1.9% 119.84 KB 121.73 KB 37.65 KB 38.36 KB NODE_PROD
ReactTestUtils-dev.js +0.1% +0.1% 52.29 KB 52.36 KB 14.27 KB 14.29 KB FB_WWW_DEV
react-dom-server.node.production.min.js 0.0% -0.0% 20.8 KB 20.8 KB 7.63 KB 7.63 KB NODE_PROD
react-dom-server.browser.development.js 0.0% -0.0% 134.84 KB 134.84 KB 35.76 KB 35.76 KB NODE_DEV
ReactDOM-dev.js +1.2% +1.3% 979.95 KB 991.75 KB 216.43 KB 219.2 KB FB_WWW_DEV
ReactDOM-prod.js 🔺+1.6% 🔺+1.5% 393.16 KB 399.52 KB 71.82 KB 72.87 KB FB_WWW_PROD
react-dom-unstable-native-dependencies.development.js 0.0% -0.0% 60.91 KB 60.91 KB 16.07 KB 16.07 KB UMD_DEV
ReactDOM-profiling.js +1.5% +1.4% 404.49 KB 410.73 KB 73.97 KB 75 KB FB_WWW_PROFILING
react-dom-unstable-native-dependencies.development.js 0.0% -0.0% 60.61 KB 60.61 KB 16 KB 15.99 KB NODE_DEV
react-dom-unstable-fizz.node.development.js 0.0% -0.1% 4.42 KB 4.42 KB 1.65 KB 1.65 KB NODE_DEV
ReactDOMServer-dev.js 0.0% 0.0% 140.24 KB 140.3 KB 35.49 KB 35.51 KB FB_WWW_DEV

react-test-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-test-renderer.development.js +1.9% +1.6% 622.89 KB 634.52 KB 131.48 KB 133.6 KB UMD_DEV
react-test-renderer.production.min.js 🔺+2.6% 🔺+3.3% 71.93 KB 73.83 KB 21.94 KB 22.67 KB UMD_PROD
react-test-renderer-shallow.development.js 0.0% -0.0% 37.89 KB 37.89 KB 9.84 KB 9.84 KB UMD_DEV
react-test-renderer.development.js +1.9% +1.6% 618.15 KB 629.79 KB 130.3 KB 132.42 KB NODE_DEV
react-test-renderer.production.min.js 🔺+2.6% 🔺+3.4% 71.63 KB 73.52 KB 21.56 KB 22.3 KB NODE_PROD
ReactTestRenderer-dev.js +1.9% +1.6% 633.98 KB 645.75 KB 131.12 KB 133.24 KB FB_WWW_DEV

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactNativeRenderer-dev.js +1.6% +1.3% 750.92 KB 762.69 KB 157.91 KB 160.03 KB RN_OSS_DEV
ReactFabric-dev.js +1.6% +1.4% 741.54 KB 753.31 KB 155.71 KB 157.81 KB RN_OSS_DEV
ReactNativeRenderer-dev.js +1.6% +1.3% 751.1 KB 762.87 KB 158.01 KB 160.12 KB RN_FB_DEV
ReactFabric-prod.js 🔺+2.5% 🔺+2.0% 266.81 KB 273.51 KB 45.77 KB 46.71 KB RN_OSS_PROD
ReactNativeRenderer-prod.js 🔺+2.4% 🔺+2.0% 274.78 KB 281.48 KB 47.1 KB 48.02 KB RN_FB_PROD
ReactFabric-profiling.js +2.4% +1.9% 277.97 KB 284.6 KB 47.9 KB 48.82 KB RN_OSS_PROFILING
ReactNativeRenderer-profiling.js +2.3% +1.9% 285.98 KB 292.61 KB 49.25 KB 50.17 KB RN_FB_PROFILING
ReactNativeRenderer-prod.js 🔺+2.4% 🔺+2.0% 274.39 KB 281.09 KB 47.03 KB 47.95 KB RN_OSS_PROD
ReactFabric-dev.js +1.6% +1.4% 741.72 KB 753.49 KB 155.79 KB 157.89 KB RN_FB_DEV
ReactNativeRenderer-profiling.js +2.3% +1.9% 285.59 KB 292.22 KB 49.18 KB 50.1 KB RN_OSS_PROFILING
ReactFabric-prod.js 🔺+2.5% 🔺+2.0% 267.16 KB 273.86 KB 45.84 KB 46.78 KB RN_FB_PROD
ReactFabric-profiling.js +2.4% +1.9% 278.31 KB 284.94 KB 47.97 KB 48.88 KB RN_FB_PROFILING

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactART-dev.js +1.9% +1.7% 622.3 KB 634.1 KB 128.56 KB 130.69 KB FB_WWW_DEV
ReactART-prod.js 🔺+2.9% 🔺+2.5% 236.29 KB 243.2 KB 39.8 KB 40.79 KB FB_WWW_PROD
react-art.development.js +1.7% +1.5% 678.11 KB 689.75 KB 145.94 KB 148.07 KB UMD_DEV
react-art.production.min.js 🔺+1.8% 🔺+2.2% 107.32 KB 109.21 KB 32.52 KB 33.24 KB UMD_PROD
react-art.development.js +1.9% +1.7% 608.79 KB 620.43 KB 128.52 KB 130.66 KB NODE_DEV
react-art.production.min.js 🔺+2.6% 🔺+3.2% 72.29 KB 74.17 KB 21.68 KB 22.37 KB NODE_PROD

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler-persistent.development.js +1.9% +1.7% 606.99 KB 618.62 KB 126.51 KB 128.7 KB NODE_DEV
react-reconciler-reflection.development.js 0.0% -0.0% 19.53 KB 19.53 KB 6.4 KB 6.4 KB NODE_DEV
react-reconciler-persistent.production.min.js 🔺+2.6% 🔺+3.4% 72.53 KB 74.42 KB 21.32 KB 22.04 KB NODE_PROD
react-reconciler-reflection.production.min.js 0.0% -0.1% 2.86 KB 2.86 KB 1.24 KB 1.24 KB NODE_PROD
react-reconciler.development.js +1.9% +1.7% 609.79 KB 621.43 KB 127.69 KB 129.86 KB NODE_DEV
react-reconciler.production.min.js 🔺+2.5% 🔺+3.1% 75.29 KB 77.18 KB 21.96 KB 22.65 KB NODE_PROD

Size changes (experimental)

Generated by 🚫 dangerJS against fe9d199

@acdlite acdlite force-pushed the rewrite-use-transition branch 2 times, most recently from ab4af79 to 7aa1292 Compare January 25, 2020 01:16
We currently use the expiration time to represent the timeout of a
transition. Since we intend to stop treating work priority as a
timeline, we can no longer use this trick.

In this commit, I've changed it to store the event time on the update
object instead. Long term, we will store event time on the root as a map
of transition -> event time. I'm only storing it on the update object
as a temporary workaround to unblock the rest of the changes.
When multiple transitions update the same queue, only the most recent
one should be considered pending.

Example: If I switch tabs multiple times, only the last tab I click
should display a pending state (e.g. an inline spinner).
When multiple transitions update the same queue, only the most recent
one should be allowed to finish. Do not display intermediate states.

For example, if you click on multiple tabs in quick succession, we
should not switch to any tab that isn't the last one you clicked.
// If `timeoutMs` is not specified, we default to 5 seconds.
// TODO: Should this default to a JND instead?
const timeoutMs = suspenseConfig.timeoutMs | 0 || LOW_PRIORITY_EXPIRATION;
const timeoutTime = computeSuspenseTimeout(eventTime, timeoutMs);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Move default timeout to when the update is scheduled

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Nevermind. The reason this is resolved here is because timeoutMs is stored on the Suspense config object, which is owned by userspace. In the future we can track this on the root as a map of transition -> timeout.

);
// If there's a SuspenseConfig, treat this as a concurrent update regardless
// of the priority.
expirationTime = computeAsyncExpiration(currentTime);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Split into two "lanes" depending on how large the timeout is.

  • Smallish numbers get currentTime + 10sec
  • "Infinite" numbers get really big number

@acdlite
Copy link
Collaborator Author

acdlite commented Jan 27, 2020

To-do

  • Go back to using useState hook for isPending queue
  • Use map of intermediateTransitionTime -> terminalTransitionTime instead of ranges of times. If a non-transition happens to fall in between, oh well.
  • Inline resolveTransitionTime into getNextExpirationTimeToWorkOn

@acdlite
Copy link
Collaborator Author

acdlite commented Jan 28, 2020

Discussed with @sebmarkbage offline. Here's the plan:

  • The ideal implementation requires a refactor of expiration times. The refactor will be significant, so I originally wanted to land the semantic changes in this PR first before moving on to a refactor. But since the interim solution in this PR is vastly different from the implementation post-refactor, we're going to do the refactor first.
  • The first commit (decoupling expiration times and timeouts) is still useful so we should land that now. I'll split this into a new PR so this one can stay as a reference.

@acdlite acdlite closed this Jan 28, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed React Core Team Opened by a member of the React Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants