Skip to content

Conversation

sebmarkbage
Copy link
Collaborator

Stacked on #32783. This will replace the useSwipeTransition API.

Instead, of a special Hook, you can make updates to useOptimistic Hooks within the startGestureTransition scope.

import {unstable_startGestureTransition as startGestureTransition} from 'react';

const cancel = startGestureTransition(timeline, () => {
  setOptimistic(...);
}, options);

There are some downsides to this like you can't define two directions as once and there's no "standard" direction protocol. It's instead up to libraries to come up with their own conventions (although we can suggest some).

The convention is still that a gesture recognizer has two props action and gesture. The gesture prop is a Gesture concept which now behaves more like an Action but 1) it can't be async 2) it shouldn't have side-effects. For example you can't call setState() in it except on useOptimistic since those can be reverted if needed. The action is invoked with whatever side-effects you want after the gesture fulfills.

This is isomorphic and not associated with a specific renderer nor root so it's a bit more complicated.

To implement this I unify with the ReactSharedInternal.T property to contain a regular Transition or a Gesture Transition (the gesture field). The benefit of this unification means that every time we override this based on some scope like entering flushSync we also override the startGestureTransition scope. We just have to be careful when we read it to check the gesture field to know which one it is. (E.g. I error for setState / requestFormReset.)

The other thing that's unique is the cancel return value to know when to stop the gesture. That cancellation is no longer associated with any particular Hook. It's more associated with the scope of the startGestureTransition. Since the schedule of whether a particular gesture has rendered or committed is associated with a root, we need to somehow associate any scheduled gestures with a root.

We could track which roots we update inside the scope but instead, I went with a model where I check all the roots and see if there's a scheduled gesture matching the timeline. This means that you could "retain" a gesture across roots. Meaning this wouldn't cancel until both are cancelled:

const cancelA = startGestureTransition(timeline, () => {
  setOptimisticOnRootA(...);
}, options);

const cancelB = startGestureTransition(timeline, () => {
  setOptimisticOnRootB(...);
}, options);

It's more like it's a global transition than associated with the roots that were updated.

Optimistic updates mostly just work but I now associate them with a specific "ScheduledGesture" instance since we can only render one at a time and so if it's not the current one, we leave it for later.

Clean up of optimistic updates is now lazy rather than when we cancel. Allowing the cancel closure not to have to be associated with each particular update.

@react-sizebot
Copy link

react-sizebot commented Mar 29, 2025

Comparing: d3b8ff6...7b719b3

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.js = 6.68 kB 6.68 kB +0.05% 1.83 kB 1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js +0.03% 515.13 kB 515.28 kB +0.03% 91.78 kB 91.81 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 6.69 kB 6.69 kB +0.11% 1.83 kB 1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js +0.61% 615.28 kB 619.04 kB +0.65% 108.88 kB 109.59 kB
facebook-www/ReactDOM-prod.classic.js +0.02% 648.06 kB 648.21 kB +0.03% 114.51 kB 114.54 kB
facebook-www/ReactDOM-prod.modern.js +0.02% 638.34 kB 638.49 kB +0.02% 112.93 kB 112.96 kB
oss-experimental/react/cjs/react.production.js +4.41% 18.36 kB 19.17 kB +2.94% 4.73 kB 4.87 kB
oss-experimental/react/cjs/react.development.js +2.71% 46.55 kB 47.81 kB +1.71% 10.61 kB 10.80 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
oss-experimental/react/cjs/react.production.js +4.41% 18.36 kB 19.17 kB +2.94% 4.73 kB 4.87 kB
oss-experimental/react/cjs/react.development.js +2.71% 46.55 kB 47.81 kB +1.71% 10.61 kB 10.80 kB
oss-experimental/react-art/cjs/react-art.development.js +1.25% 635.79 kB 643.75 kB +1.70% 101.11 kB 102.84 kB
oss-experimental/react-reconciler/cjs/react-reconciler.production.js +0.88% 462.43 kB 466.50 kB +0.89% 74.32 kB 74.98 kB
oss-experimental/react-reconciler/cjs/react-reconciler.profiling.js +0.79% 515.13 kB 519.21 kB +0.76% 82.23 kB 82.86 kB
facebook-www/React-prod.modern.js +0.78% 20.52 kB 20.68 kB +0.70% 5.26 kB 5.30 kB
facebook-www/React-prod.classic.js +0.78% 20.52 kB 20.68 kB +0.70% 5.26 kB 5.30 kB
facebook-www/React-profiling.modern.js +0.77% 20.95 kB 21.11 kB +0.71% 5.34 kB 5.38 kB
facebook-www/React-profiling.classic.js +0.77% 20.95 kB 21.11 kB +0.73% 5.34 kB 5.38 kB
oss-experimental/react-reconciler/cjs/react-reconciler.development.js +0.70% 761.63 kB 766.96 kB +0.89% 120.06 kB 121.13 kB
oss-experimental/react-art/cjs/react-art.production.js +0.67% 337.32 kB 339.59 kB +0.80% 57.16 kB 57.62 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js +0.61% 615.28 kB 619.04 kB +0.65% 108.88 kB 109.59 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.production.js +0.60% 629.69 kB 633.45 kB +0.66% 112.42 kB 113.17 kB
oss-experimental/react-dom/cjs/react-dom-profiling.profiling.js +0.56% 671.07 kB 674.83 kB +0.63% 117.77 kB 118.51 kB
oss-experimental/react-dom/cjs/react-dom-client.development.js +0.48% 1,105.01 kB 1,110.32 kB +0.58% 184.71 kB 185.78 kB
oss-experimental/react-dom/cjs/react-dom-profiling.development.js +0.47% 1,121.40 kB 1,126.71 kB +0.57% 187.55 kB 188.62 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.development.js +0.47% 1,121.55 kB 1,126.86 kB +0.57% 188.35 kB 189.42 kB
facebook-www/React-dev.modern.js +0.34% 54.82 kB 55.00 kB +0.17% 11.97 kB 11.99 kB
facebook-www/React-dev.classic.js +0.34% 54.82 kB 55.01 kB +0.17% 11.97 kB 11.99 kB
facebook-react-native/react/cjs/React-dev.js = 51.03 kB 50.76 kB = 11.35 kB 11.29 kB
facebook-react-native/react/cjs/React-profiling.js = 19.60 kB 19.36 kB = 5.06 kB 5.01 kB
facebook-react-native/react/cjs/React-prod.js = 19.17 kB 18.93 kB = 4.98 kB 4.93 kB

Generated by 🚫 dangerJS against 7b719b3

…ureTransition

setState on useOptimistic is allowed and schedules on the gesture lane.
useSwipeTransition is still required to start the gesture but optimistic
can be used for the actual state.
Now we can start the gesture just by calling setState on useOptimistic.
…th the currently rendering gesture

Otherwise we leave it behind for future gesture lane renders to consume.

Clean up happens lazily instead of the cancel function having a closure over every update.
@sebmarkbage sebmarkbage force-pushed the startgesturetransition branch from 462caa2 to 7b719b3 Compare April 1, 2025 00:01
@sebmarkbage sebmarkbage merged commit b286430 into facebook:main Apr 1, 2025
241 checks passed
github-actions bot pushed a commit that referenced this pull request Apr 1, 2025
Stacked on #32783. This will replace [the `useSwipeTransition`
API](#32373).

Instead, of a special Hook, you can make updates to `useOptimistic`
Hooks within the `startGestureTransition` scope.

```
import {unstable_startGestureTransition as startGestureTransition} from 'react';

const cancel = startGestureTransition(timeline, () => {
  setOptimistic(...);
}, options);
```

There are some downsides to this like you can't define two directions as
once and there's no "standard" direction protocol. It's instead up to
libraries to come up with their own conventions (although we can suggest
some).

The convention is still that a gesture recognizer has two props `action`
and `gesture`. The `gesture` prop is a Gesture concept which now behaves
more like an Action but 1) it can't be async 2) it shouldn't have
side-effects. For example you can't call `setState()` in it except on
`useOptimistic` since those can be reverted if needed. The `action` is
invoked with whatever side-effects you want after the gesture fulfills.

This is isomorphic and not associated with a specific renderer nor root
so it's a bit more complicated.

To implement this I unify with the `ReactSharedInternal.T` property to
contain a regular Transition or a Gesture Transition (the `gesture`
field). The benefit of this unification means that every time we
override this based on some scope like entering `flushSync` we also
override the `startGestureTransition` scope. We just have to be careful
when we read it to check the `gesture` field to know which one it is.
(E.g. I error for setState / requestFormReset.)

The other thing that's unique is the `cancel` return value to know when
to stop the gesture. That cancellation is no longer associated with any
particular Hook. It's more associated with the scope of the
`startGestureTransition`. Since the schedule of whether a particular
gesture has rendered or committed is associated with a root, we need to
somehow associate any scheduled gestures with a root.

We could track which roots we update inside the scope but instead, I
went with a model where I check all the roots and see if there's a
scheduled gesture matching the timeline. This means that you could
"retain" a gesture across roots. Meaning this wouldn't cancel until both
are cancelled:

```
const cancelA = startGestureTransition(timeline, () => {
  setOptimisticOnRootA(...);
}, options);

const cancelB = startGestureTransition(timeline, () => {
  setOptimisticOnRootB(...);
}, options);
```

It's more like it's a global transition than associated with the roots
that were updated.

Optimistic updates mostly just work but I now associate them with a
specific "ScheduledGesture" instance since we can only render one at a
time and so if it's not the current one, we leave it for later.

Clean up of optimistic updates is now lazy rather than when we cancel.
Allowing the cancel closure not to have to be associated with each
particular update.

DiffTrain build for [b286430](b286430)
github-actions bot pushed a commit that referenced this pull request Apr 1, 2025
Stacked on #32783. This will replace [the `useSwipeTransition`
API](#32373).

Instead, of a special Hook, you can make updates to `useOptimistic`
Hooks within the `startGestureTransition` scope.

```
import {unstable_startGestureTransition as startGestureTransition} from 'react';

const cancel = startGestureTransition(timeline, () => {
  setOptimistic(...);
}, options);
```

There are some downsides to this like you can't define two directions as
once and there's no "standard" direction protocol. It's instead up to
libraries to come up with their own conventions (although we can suggest
some).

The convention is still that a gesture recognizer has two props `action`
and `gesture`. The `gesture` prop is a Gesture concept which now behaves
more like an Action but 1) it can't be async 2) it shouldn't have
side-effects. For example you can't call `setState()` in it except on
`useOptimistic` since those can be reverted if needed. The `action` is
invoked with whatever side-effects you want after the gesture fulfills.

This is isomorphic and not associated with a specific renderer nor root
so it's a bit more complicated.

To implement this I unify with the `ReactSharedInternal.T` property to
contain a regular Transition or a Gesture Transition (the `gesture`
field). The benefit of this unification means that every time we
override this based on some scope like entering `flushSync` we also
override the `startGestureTransition` scope. We just have to be careful
when we read it to check the `gesture` field to know which one it is.
(E.g. I error for setState / requestFormReset.)

The other thing that's unique is the `cancel` return value to know when
to stop the gesture. That cancellation is no longer associated with any
particular Hook. It's more associated with the scope of the
`startGestureTransition`. Since the schedule of whether a particular
gesture has rendered or committed is associated with a root, we need to
somehow associate any scheduled gestures with a root.

We could track which roots we update inside the scope but instead, I
went with a model where I check all the roots and see if there's a
scheduled gesture matching the timeline. This means that you could
"retain" a gesture across roots. Meaning this wouldn't cancel until both
are cancelled:

```
const cancelA = startGestureTransition(timeline, () => {
  setOptimisticOnRootA(...);
}, options);

const cancelB = startGestureTransition(timeline, () => {
  setOptimisticOnRootB(...);
}, options);
```

It's more like it's a global transition than associated with the roots
that were updated.

Optimistic updates mostly just work but I now associate them with a
specific "ScheduledGesture" instance since we can only render one at a
time and so if it's not the current one, we leave it for later.

Clean up of optimistic updates is now lazy rather than when we cancel.
Allowing the cancel closure not to have to be associated with each
particular update.

DiffTrain build for [b286430](b286430)
sebmarkbage added a commit that referenced this pull request Apr 1, 2025
Stacked on #32785.

This is now replaced by `startGestureTransition` added in #32785.

I also renamed the flag from `enableSwipeTransition` to
`enableGestureTransition` to correspond to the new name.
github-actions bot pushed a commit that referenced this pull request Apr 1, 2025
Stacked on #32785.

This is now replaced by `startGestureTransition` added in #32785.

I also renamed the flag from `enableSwipeTransition` to
`enableGestureTransition` to correspond to the new name.

DiffTrain build for [0a7cf20](0a7cf20)
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