-
Notifications
You must be signed in to change notification settings - Fork 47.1k
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
Conversation
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:
|
Details of bundled changes.Comparing: cf00812...fe9d199 react-art
react-reconciler
react-native-renderer
react-dom
react-test-renderer
ReactDOM: size: 🔺+1.4%, gzip: 🔺+1.8% Size changes (stable) |
Details of bundled changes.Comparing: cf00812...fe9d199 react-dom
react-test-renderer
react-native-renderer
react-art
react-reconciler
Size changes (experimental) |
ab4af79
to
7aa1292
Compare
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.
7aa1292
to
fe9d199
Compare
// 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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
To-do
|
Discussed with @sebmarkbage offline. Here's the plan:
|
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 theuseTransition
hooks that correspond to the intermediate states are turned off (unless they happen to be the sameuseTransition
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