@samitouri / QOS-React / commits / a53da6abe1

Add useSwipeTransition Hook Behind Experimental Flag (#32373)

This Hook will be used to drive a View Transition based on a gesture. ```js const [value, startGesture] = useSwipeTransition(prev, current, next); ``` The `enableSwipeTransition` flag will depend on `enableViewTransition` flag but we may decide to ship them independently. This PR doesn't do anything interesting yet. There will be a lot more PRs to build out the actual functionality. This is just wiring up the plumbing for the new Hook. This first PR is mainly concerned with how the whole starts (and stops). The core API is the `startGesture` function (although there will be other conveniences added in the future). You can call this to start a gesture with a source provider. You can call this multiple times in one event to batch multiple Hooks listening to the same provider. However, each render can only handle one source provider at a time and so it does one render per scheduled gesture provider. This uses a separate `GestureLane` to drive gesture renders by marking the Hook as having an update on that lane. Then schedule a render. These renders should be blocking and in the same microtask as the `startGesture` to ensure it can block the paint. So it's similar to sync. It may not be possible to finish it synchronously e.g. if something suspends. If so, it just tries again later when it can like any other render. This can also happen because it also may not be possible to drive more than one gesture at a time like if we're limited to one View Transition per document. So right now you can only run one gesture at a time in practice. These renders never commit. This means that we can't clear the `GestureLane` the normal way. Instead, we have to clear only the root's `pendingLanes` if we don't have any new renders scheduled. Then wait until something else updates the Fiber after all gestures on it have stopped before it really clears.

Sebastian Markbåge committed Feb 13, 2025 at 16:06 UTC a53da6abe1593483098df2baf927fe07d80153a5
26 files changed +647 -68
fixtures/view-transition/src/components/Page.css
+11
@@ -6,3 +6,14 @@
6 font-variation-settings:
7 "wdth" 100;
8 }
9 +
10 +.swipe-recognizer {
11 + width: 200px;
12 + overflow-x: scroll;
13 + border: 1px solid #333333;
14 + border-radius: 10px;
15 +}
16 +
17 +.swipe-overscroll {
18 + width: 200%;
19 +}
fixtures/view-transition/src/components/Page.js
+38 -1
@@ -1,6 +1,9 @@
1 import React, {
2 unstable_ViewTransition as ViewTransition,
3 unstable_Activity as Activity,
4 + unstable_useSwipeTransition as useSwipeTransition,
5 + useRef,
6 + useLayoutEffect,
7 } from 'react';
8
9 import './Page.css';
@@ -35,7 +38,8 @@ function Component() {
38 }
39
40 export default function Page({url, navigate}) {
38 - const show = url === '/?b';
41 + const [renderedUrl, startGesture] = useSwipeTransition('/?a', url, '/?b');
42 + const show = renderedUrl === '/?b';
43 function onTransition(viewTransition, types) {
44 const keyframes = [
45 {rotate: '0deg', transformOrigin: '30px 8px'},
@@ -44,6 +48,32 @@ export default function Page({url, navigate}) {
48 viewTransition.old.animate(keyframes, 250);
49 viewTransition.new.animate(keyframes, 250);
50 }
51 +
52 + const swipeRecognizer = useRef(null);
53 + const activeGesture = useRef(null);
54 + function onScroll() {
55 + if (activeGesture.current !== null) {
56 + return;
57 + }
58 + // eslint-disable-next-line no-undef
59 + const scrollTimeline = new ScrollTimeline({
60 + source: swipeRecognizer.current,
61 + axis: 'x',
62 + });
63 + activeGesture.current = startGesture(scrollTimeline);
64 + }
65 + function onScrollEnd() {
66 + if (activeGesture.current !== null) {
67 + const cancelGesture = activeGesture.current;
68 + activeGesture.current = null;
69 + cancelGesture();
70 + }
71 + }
72 +
73 + useLayoutEffect(() => {
74 + swipeRecognizer.current.scrollLeft = show ? 0 : 10000;
75 + }, [show]);
76 +
77 const exclamation = (
78 <ViewTransition name="exclamation" onShare={onTransition}>
79 <span>!</span>
@@ -90,6 +120,13 @@ export default function Page({url, navigate}) {
120 <p></p>
121 <p></p>
122 <p></p>
123 + <div
124 + className="swipe-recognizer"
125 + onScroll={onScroll}
126 + onScrollEnd={onScrollEnd}
127 + ref={swipeRecognizer}>
128 + <div className="swipe-overscroll">Swipe me</div>
129 + </div>
130 <p></p>
131 <p></p>
132 {show ? null : (
packages/react-debug-tools/src/ReactDebugHooks.js
+30 -7
@@ -14,6 +14,7 @@ import type {
14 Usable,
15 Thenable,
16 ReactDebugInfo,
17 + StartGesture,
18 } from 'shared/ReactTypes';
19 import type {
20 ContextDependency,
@@ -131,6 +132,9 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
132 if (typeof Dispatcher.useEffectEvent === 'function') {
133 Dispatcher.useEffectEvent((args: empty) => {});
134 }
135 + if (typeof Dispatcher.useSwipeTransition === 'function') {
136 + Dispatcher.useSwipeTransition(null, null, null);
137 + }
138 } finally {
139 readHookLog = hookLog;
140 hookLog = [];
@@ -752,31 +756,50 @@ function useEffectEvent<Args, F: (...Array<Args>) => mixed>(callback: F): F {
756 return callback;
757 }
758
759 +function useSwipeTransition<T>(
760 + previous: T,
761 + current: T,
762 + next: T,
763 +): [T, StartGesture] {
764 + nextHook();
765 + hookLog.push({
766 + displayName: null,
767 + primitive: 'SwipeTransition',
768 + stackError: new Error(),
769 + value: current,
770 + debugInfo: null,
771 + dispatcherHookName: 'SwipeTransition',
772 + });
773 + return [current, () => () => {}];
774 +}
775 +
776 const Dispatcher: DispatcherType = {
756 - use,
777 readContext,
758 - useCacheRefresh,
778 +
779 + use,
780 useCallback,
781 useContext,
782 useEffect,
783 useImperativeHandle,
763 - useDebugValue,
784 useLayoutEffect,
785 useInsertionEffect,
786 useMemo,
767 - useMemoCache,
768 - useOptimistic,
787 useReducer,
788 useRef,
789 useState,
790 + useDebugValue,
791 + useDeferredValue,
792 useTransition,
793 useSyncExternalStore,
774 - useDeferredValue,
794 useId,
795 + useHostTransitionStatus,
796 useFormState,
797 useActionState,
778 - useHostTransitionStatus,
798 + useOptimistic,
799 + useMemoCache,
800 + useCacheRefresh,
801 useEffectEvent,
802 + useSwipeTransition,
803 };
804
805 // create a proxy to throw a custom error
packages/react-reconciler/src/ReactFiberConcurrentUpdates.js
+34 -4
@@ -24,7 +24,14 @@ import {
24 throwIfInfiniteUpdateLoopDetected,
25 getWorkInProgressRoot,
26 } from './ReactFiberWorkLoop';
27 -import {NoLane, NoLanes, mergeLanes, markHiddenUpdate} from './ReactFiberLane';
27 +import {
28 + NoLane,
29 + NoLanes,
30 + mergeLanes,
31 + markHiddenUpdate,
32 + markRootUpdated,
33 + GestureLane,
34 +} from './ReactFiberLane';
35 import {NoFlags, Placement, Hydrating} from './ReactFiberFlags';
36 import {HostRoot, OffscreenComponent} from './ReactWorkTags';
37 import {OffscreenVisible} from './ReactFiberActivityComponent';
@@ -169,6 +176,25 @@ export function enqueueConcurrentRenderForLane(
176 return getRootForUpdatedFiber(fiber);
177 }
178
179 +export function enqueueGestureRender(fiber: Fiber): FiberRoot | null {
180 + // We can't use the concurrent queuing for these so this is basically just a
181 + // short cut for marking the lane on the parent path. It is possible for a
182 + // gesture render to suspend and then in the gap get another gesture starting.
183 + // However, marking the lane doesn't make much different in this case because
184 + // it would have to call startGesture with the same exact provider as was
185 + // already rendering. Because otherwise it has no effect on the Hook itself.
186 + // TODO: We could potentially solve this case by popping a ScheduledGesture
187 + // off the root's queue while we're rendering it so that it can't dedupe
188 + // and so new startGesture with the same provider would create a new
189 + // ScheduledGesture which goes into a separate render pass anyway.
190 + // This is such an edge case it probably doesn't matter much.
191 + const root = markUpdateLaneFromFiberToRoot(fiber, null, GestureLane);
192 + if (root !== null) {
193 + markRootUpdated(root, GestureLane);
194 + }
195 + return root;
196 +}
197 +
198 // Calling this function outside this module should only be done for backwards
199 // compatibility and should always be accompanied by a warning.
200 export function unsafe_markUpdateLaneFromFiberToRoot(
@@ -189,7 +215,7 @@ function markUpdateLaneFromFiberToRoot(
215 sourceFiber: Fiber,
216 update: ConcurrentUpdate | null,
217 lane: Lane,
192 -): void {
218 +): null | FiberRoot {
219 // Update the source fiber's lanes
220 sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
221 let alternate = sourceFiber.alternate;
@@ -238,10 +264,14 @@ function markUpdateLaneFromFiberToRoot(
264 parent = parent.return;
265 }
266
241 - if (isHidden && update !== null && node.tag === HostRoot) {
267 + if (node.tag === HostRoot) {
268 const root: FiberRoot = node.stateNode;
243 - markHiddenUpdate(root, update, lane);
269 + if (isHidden && update !== null) {
270 + markHiddenUpdate(root, update, lane);
271 + }
272 + return root;
273 }
274 + return null;
275 }
276
277 function getRootForUpdatedFiber(sourceFiber: Fiber): FiberRoot | null {
packages/react-reconciler/src/ReactFiberGestureScheduler.js new
+90
@@ -0,0 +1,90 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {FiberRoot} from './ReactInternalTypes';
11 +import type {GestureProvider} from 'shared/ReactTypes';
12 +
13 +import {GestureLane} from './ReactFiberLane';
14 +import {ensureRootIsScheduled} from './ReactFiberRootScheduler';
15 +
16 +// This type keeps track of any scheduled or active gestures.
17 +export type ScheduledGesture = {
18 + provider: GestureProvider,
19 + count: number, // The number of times this same provider has been started.
20 + prev: null | ScheduledGesture, // The previous scheduled gesture in the queue for this root.
21 + next: null | ScheduledGesture, // The next scheduled gesture in the queue for this root.
22 +};
23 +
24 +export function scheduleGesture(
25 + root: FiberRoot,
26 + provider: GestureProvider,
27 +): ScheduledGesture {
28 + let prev = root.gestures;
29 + while (prev !== null) {
30 + if (prev.provider === provider) {
31 + // Existing instance found.
32 + prev.count++;
33 + return prev;
34 + }
35 + const next = prev.next;
36 + if (next === null) {
37 + break;
38 + }
39 + prev = next;
40 + }
41 + // Add new instance to the end of the queue.
42 + const gesture: ScheduledGesture = {
43 + provider: provider,
44 + count: 1,
45 + prev: prev,
46 + next: null,
47 + };
48 + if (prev === null) {
49 + root.gestures = gesture;
50 + } else {
51 + prev.next = gesture;
52 + }
53 + ensureRootIsScheduled(root);
54 + return gesture;
55 +}
56 +
57 +export function cancelScheduledGesture(
58 + root: FiberRoot,
59 + gesture: ScheduledGesture,
60 +): void {
61 + gesture.count--;
62 + if (gesture.count === 0) {
63 + // Delete the scheduled gesture from the queue.
64 + deleteScheduledGesture(root, gesture);
65 + }
66 +}
67 +
68 +export function deleteScheduledGesture(
69 + root: FiberRoot,
70 + gesture: ScheduledGesture,
71 +): void {
72 + if (gesture.prev === null) {
73 + if (root.gestures === gesture) {
74 + root.gestures = gesture.next;
75 + if (root.gestures === null) {
76 + // Gestures don't clear their lanes while the gesture is still active but it
77 + // might not be scheduled to do any more renders and so we shouldn't schedule
78 + // any more gesture lane work until a new gesture is scheduled.
79 + root.pendingLanes &= ~GestureLane;
80 + }
81 + }
82 + } else {
83 + gesture.prev.next = gesture.next;
84 + if (gesture.next !== null) {
85 + gesture.next.prev = gesture.prev;
86 + }
87 + gesture.prev = null;
88 + gesture.next = null;
89 + }
90 +}
packages/react-reconciler/src/ReactFiberHooks.js
+242
@@ -14,6 +14,8 @@ import type {
14 Thenable,
15 RejectedThenable,
16 Awaited,
17 + StartGesture,
18 + GestureProvider,
19 } from 'shared/ReactTypes';
20 import type {
21 Fiber,
@@ -26,6 +28,7 @@ import type {Lanes, Lane} from './ReactFiberLane';
28 import type {HookFlags} from './ReactHookEffectTags';
29 import type {Flags} from './ReactFiberFlags';
30 import type {TransitionStatus} from './ReactFiberConfig';
31 +import type {ScheduledGesture} from './ReactFiberGestureScheduler';
32
33 import {
34 HostTransitionContext,
@@ -42,6 +45,7 @@ import {
45 enableLegacyCache,
46 disableLegacyMode,
47 enableNoCloningMemoCache,
48 + enableSwipeTransition,
49 } from 'shared/ReactFeatureFlags';
50 import {
51 REACT_CONTEXT_TYPE,
@@ -70,6 +74,8 @@ import {
74 isTransitionLane,
75 markRootEntangled,
76 includesSomeLane,
77 + isGestureRender,
78 + GestureLane,
79 } from './ReactFiberLane';
80 import {
81 ContinuousEventPriority,
@@ -130,6 +136,7 @@ import {
136 enqueueConcurrentHookUpdate,
137 enqueueConcurrentHookUpdateAndEagerlyBailout,
138 enqueueConcurrentRenderForLane,
139 + enqueueGestureRender,
140 } from './ReactFiberConcurrentUpdates';
141 import {getTreeId} from './ReactFiberTreeContext';
142 import {now} from './Scheduler';
@@ -153,6 +160,11 @@ import {requestCurrentTransition} from './ReactFiberTransition';
160
161 import {callComponentInDEV} from './ReactFiberCallUserSpace';
162
163 +import {
164 + scheduleGesture,
165 + cancelScheduledGesture,
166 +} from './ReactFiberGestureScheduler';
167 +
168 export type Update<S, A> = {
169 lane: Lane,
170 revertLane: Lane,
@@ -3960,6 +3972,133 @@ function markUpdateInDevTools<A>(fiber: Fiber, lane: Lane, action: A): void {
3972 }
3973 }
3974
3975 +type SwipeTransitionGestureUpdate = {
3976 + gesture: ScheduledGesture,
3977 + prev: SwipeTransitionGestureUpdate | null,
3978 + next: SwipeTransitionGestureUpdate | null,
3979 +};
3980 +
3981 +type SwipeTransitionUpdateQueue = {
3982 + pending: null | SwipeTransitionGestureUpdate,
3983 + dispatch: StartGesture,
3984 +};
3985 +
3986 +function startGesture(
3987 + fiber: Fiber,
3988 + queue: SwipeTransitionUpdateQueue,
3989 + gestureProvider: GestureProvider,
3990 +): () => void {
3991 + const root = enqueueGestureRender(fiber);
3992 + if (root === null) {
3993 + // Already unmounted.
3994 + // TODO: Should we warn here about starting on an unmounted Fiber?
3995 + return function cancelGesture() {
3996 + // Noop.
3997 + };
3998 + }
3999 + const scheduledGesture = scheduleGesture(root, gestureProvider);
4000 + // Add this particular instance to the queue.
4001 + // We add multiple of the same provider even if they get batched so
4002 + // that if we cancel one but not the other we can keep track of this.
4003 + // Order doesn't matter but we insert in the beginning to avoid two fields.
4004 + const update: SwipeTransitionGestureUpdate = {
4005 + gesture: scheduledGesture,
4006 + prev: null,
4007 + next: queue.pending,
4008 + };
4009 + if (queue.pending !== null) {
4010 + queue.pending.prev = update;
4011 + }
4012 + queue.pending = update;
4013 + return function cancelGesture(): void {
4014 + if (update.prev === null) {
4015 + if (queue.pending === update) {
4016 + queue.pending = update.next;
4017 + } else {
4018 + // This was already cancelled. Avoid double decrementing if someone calls this twice by accident.
4019 + // TODO: Should we warn here about double cancelling?
4020 + return;
4021 + }
4022 + } else {
4023 + update.prev.next = update.next;
4024 + if (update.next !== null) {
4025 + update.next.prev = update.prev;
4026 + }
4027 + update.prev = null;
4028 + update.next = null;
4029 + }
4030 + const cancelledGestured = update.gesture;
4031 + // Decrement ref count of the root schedule.
4032 + cancelScheduledGesture(root, cancelledGestured);
4033 + };
4034 +}
4035 +
4036 +function mountSwipeTransition<T>(
4037 + previous: T,
4038 + current: T,
4039 + next: T,
4040 +): [T, StartGesture] {
4041 + const queue: SwipeTransitionUpdateQueue = {
4042 + pending: null,
4043 + dispatch: (null: any),
4044 + };
4045 + const startGestureOnHook: StartGesture = (queue.dispatch = (startGesture.bind(
4046 + null,
4047 + currentlyRenderingFiber,
4048 + queue,
4049 + ): any));
4050 + const hook = mountWorkInProgressHook();
4051 + hook.queue = queue;
4052 + return [current, startGestureOnHook];
4053 +}
4054 +
4055 +function updateSwipeTransition<T>(
4056 + previous: T,
4057 + current: T,
4058 + next: T,
4059 +): [T, StartGesture] {
4060 + const hook = updateWorkInProgressHook();
4061 + const queue: SwipeTransitionUpdateQueue = hook.queue;
4062 + const startGestureOnHook: StartGesture = queue.dispatch;
4063 + const rootRenderLanes = getWorkInProgressRootRenderLanes();
4064 + let value = current;
4065 + if (isGestureRender(rootRenderLanes)) {
4066 + // We're inside a gesture render. We'll traverse the queue to see if
4067 + // this specific Hook is part of this gesture and, if so, which
4068 + // direction to render.
4069 + const root: FiberRoot | null = getWorkInProgressRoot();
4070 + if (root === null) {
4071 + throw new Error(
4072 + 'Expected a work-in-progress root. This is a bug in React. Please file an issue.',
4073 + );
4074 + }
4075 + // We assume that the currently rendering gesture is the one first in the queue.
4076 + const rootRenderGesture = root.gestures;
4077 + let update = queue.pending;
4078 + while (update !== null) {
4079 + if (rootRenderGesture === update.gesture) {
4080 + // We had a match, meaning we're currently rendering a direction of this
4081 + // hook for this gesture.
4082 + // TODO: Determine which direction this gesture is currently rendering.
4083 + value = previous;
4084 + break;
4085 + }
4086 + update = update.next;
4087 + }
4088 + }
4089 + if (queue.pending !== null) {
4090 + // As long as there are any active gestures we need to leave the lane on
4091 + // in case we need to render it later. Since a gesture render doesn't commit
4092 + // the only time it really fully gets cleared is if something else rerenders
4093 + // this component after all the active gestures has cleared.
4094 + currentlyRenderingFiber.lanes = mergeLanes(
4095 + currentlyRenderingFiber.lanes,
4096 + GestureLane,
4097 + );
4098 + }
4099 + return [value, startGestureOnHook];
4100 +}
4101 +
4102 export const ContextOnlyDispatcher: Dispatcher = {
4103 readContext,
4104
@@ -3989,6 +4128,10 @@ export const ContextOnlyDispatcher: Dispatcher = {
4128 if (enableUseEffectEventHook) {
4129 (ContextOnlyDispatcher: Dispatcher).useEffectEvent = throwInvalidHookError;
4130 }
4131 +if (enableSwipeTransition) {
4132 + (ContextOnlyDispatcher: Dispatcher).useSwipeTransition =
4133 + throwInvalidHookError;
4134 +}
4135
4136 const HooksDispatcherOnMount: Dispatcher = {
4137 readContext,
@@ -4019,6 +4162,10 @@ const HooksDispatcherOnMount: Dispatcher = {
4162 if (enableUseEffectEventHook) {
4163 (HooksDispatcherOnMount: Dispatcher).useEffectEvent = mountEvent;
4164 }
4165 +if (enableSwipeTransition) {
4166 + (HooksDispatcherOnMount: Dispatcher).useSwipeTransition =
4167 + mountSwipeTransition;
4168 +}
4169
4170 const HooksDispatcherOnUpdate: Dispatcher = {
4171 readContext,
@@ -4049,6 +4196,10 @@ const HooksDispatcherOnUpdate: Dispatcher = {
4196 if (enableUseEffectEventHook) {
4197 (HooksDispatcherOnUpdate: Dispatcher).useEffectEvent = updateEvent;
4198 }
4199 +if (enableSwipeTransition) {
4200 + (HooksDispatcherOnUpdate: Dispatcher).useSwipeTransition =
4201 + updateSwipeTransition;
4202 +}
4203
4204 const HooksDispatcherOnRerender: Dispatcher = {
4205 readContext,
@@ -4079,6 +4230,10 @@ const HooksDispatcherOnRerender: Dispatcher = {
4230 if (enableUseEffectEventHook) {
4231 (HooksDispatcherOnRerender: Dispatcher).useEffectEvent = updateEvent;
4232 }
4233 +if (enableSwipeTransition) {
4234 + (HooksDispatcherOnRerender: Dispatcher).useSwipeTransition =
4235 + updateSwipeTransition;
4236 +}
4237
4238 let HooksDispatcherOnMountInDEV: Dispatcher | null = null;
4239 let HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher | null = null;
@@ -4296,6 +4451,18 @@ if (__DEV__) {
4451 return mountEvent(callback);
4452 };
4453 }
4454 + if (enableSwipeTransition) {
4455 + (HooksDispatcherOnMountInDEV: Dispatcher).useSwipeTransition =
4456 + function useSwipeTransition<T>(
4457 + previous: T,
4458 + current: T,
4459 + next: T,
4460 + ): [T, StartGesture] {
4461 + currentHookNameInDev = 'useSwipeTransition';
4462 + mountHookTypesDev();
4463 + return mountSwipeTransition(previous, current, next);
4464 + };
4465 + }
4466
4467 HooksDispatcherOnMountWithHookTypesInDEV = {
4468 readContext<T>(context: ReactContext<T>): T {
@@ -4479,6 +4646,18 @@ if (__DEV__) {
4646 return mountEvent(callback);
4647 };
4648 }
4649 + if (enableSwipeTransition) {
4650 + (HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useSwipeTransition =
4651 + function useSwipeTransition<T>(
4652 + previous: T,
4653 + current: T,
4654 + next: T,
4655 + ): [T, StartGesture] {
4656 + currentHookNameInDev = 'useSwipeTransition';
4657 + updateHookTypesDev();
4658 + return updateSwipeTransition(previous, current, next);
4659 + };
4660 + }
4661
4662 HooksDispatcherOnUpdateInDEV = {
4663 readContext<T>(context: ReactContext<T>): T {
@@ -4662,6 +4841,18 @@ if (__DEV__) {
4841 return updateEvent(callback);
4842 };
4843 }
4844 + if (enableSwipeTransition) {
4845 + (HooksDispatcherOnUpdateInDEV: Dispatcher).useSwipeTransition =
4846 + function useSwipeTransition<T>(
4847 + previous: T,
4848 + current: T,
4849 + next: T,
4850 + ): [T, StartGesture] {
4851 + currentHookNameInDev = 'useSwipeTransition';
4852 + updateHookTypesDev();
4853 + return updateSwipeTransition(previous, current, next);
4854 + };
4855 + }
4856
4857 HooksDispatcherOnRerenderInDEV = {
4858 readContext<T>(context: ReactContext<T>): T {
@@ -4845,6 +5036,18 @@ if (__DEV__) {
5036 return updateEvent(callback);
5037 };
5038 }
5039 + if (enableSwipeTransition) {
5040 + (HooksDispatcherOnRerenderInDEV: Dispatcher).useSwipeTransition =
5041 + function useSwipeTransition<T>(
5042 + previous: T,
5043 + current: T,
5044 + next: T,
5045 + ): [T, StartGesture] {
5046 + currentHookNameInDev = 'useSwipeTransition';
5047 + updateHookTypesDev();
5048 + return updateSwipeTransition(previous, current, next);
5049 + };
5050 + }
5051
5052 InvalidNestedHooksDispatcherOnMountInDEV = {
5053 readContext<T>(context: ReactContext<T>): T {
@@ -5053,6 +5256,19 @@ if (__DEV__) {
5256 return mountEvent(callback);
5257 };
5258 }
5259 + if (enableSwipeTransition) {
5260 + (InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useSwipeTransition =
5261 + function useSwipeTransition<T>(
5262 + previous: T,
5263 + current: T,
5264 + next: T,
5265 + ): [T, StartGesture] {
5266 + currentHookNameInDev = 'useSwipeTransition';
5267 + warnInvalidHookAccess();
5268 + mountHookTypesDev();
5269 + return mountSwipeTransition(previous, current, next);
5270 + };
5271 + }
5272
5273 InvalidNestedHooksDispatcherOnUpdateInDEV = {
5274 readContext<T>(context: ReactContext<T>): T {
@@ -5261,6 +5477,19 @@ if (__DEV__) {
5477 return updateEvent(callback);
5478 };
5479 }
5480 + if (enableSwipeTransition) {
5481 + (InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useSwipeTransition =
5482 + function useSwipeTransition<T>(
5483 + previous: T,
5484 + current: T,
5485 + next: T,
5486 + ): [T, StartGesture] {
5487 + currentHookNameInDev = 'useSwipeTransition';
5488 + warnInvalidHookAccess();
5489 + updateHookTypesDev();
5490 + return updateSwipeTransition(previous, current, next);
5491 + };
5492 + }
5493
5494 InvalidNestedHooksDispatcherOnRerenderInDEV = {
5495 readContext<T>(context: ReactContext<T>): T {
@@ -5469,4 +5698,17 @@ if (__DEV__) {
5698 return updateEvent(callback);
5699 };
5700 }
5701 + if (enableSwipeTransition) {
5702 + (InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useSwipeTransition =
5703 + function useSwipeTransition<T>(
5704 + previous: T,
5705 + current: T,
5706 + next: T,
5707 + ): [T, StartGesture] {
5708 + currentHookNameInDev = 'useSwipeTransition';
5709 + warnInvalidHookAccess();
5710 + updateHookTypesDev();
5711 + return updateSwipeTransition(previous, current, next);
5712 + };
5713 + }
5714 }
packages/react-reconciler/src/ReactFiberLane.js
+30 -22
@@ -54,23 +54,24 @@ export const DefaultLane: Lane = /* */ 0b0000000000000000000
54 export const SyncUpdateLanes: Lane =
55 SyncLane | InputContinuousLane | DefaultLane;
56
57 -const TransitionHydrationLane: Lane = /* */ 0b0000000000000000000000001000000;
58 -const TransitionLanes: Lanes = /* */ 0b0000000001111111111111110000000;
59 -const TransitionLane1: Lane = /* */ 0b0000000000000000000000010000000;
60 -const TransitionLane2: Lane = /* */ 0b0000000000000000000000100000000;
61 -const TransitionLane3: Lane = /* */ 0b0000000000000000000001000000000;
62 -const TransitionLane4: Lane = /* */ 0b0000000000000000000010000000000;
63 -const TransitionLane5: Lane = /* */ 0b0000000000000000000100000000000;
64 -const TransitionLane6: Lane = /* */ 0b0000000000000000001000000000000;
65 -const TransitionLane7: Lane = /* */ 0b0000000000000000010000000000000;
66 -const TransitionLane8: Lane = /* */ 0b0000000000000000100000000000000;
67 -const TransitionLane9: Lane = /* */ 0b0000000000000001000000000000000;
68 -const TransitionLane10: Lane = /* */ 0b0000000000000010000000000000000;
69 -const TransitionLane11: Lane = /* */ 0b0000000000000100000000000000000;
70 -const TransitionLane12: Lane = /* */ 0b0000000000001000000000000000000;
71 -const TransitionLane13: Lane = /* */ 0b0000000000010000000000000000000;
72 -const TransitionLane14: Lane = /* */ 0b0000000000100000000000000000000;
73 -const TransitionLane15: Lane = /* */ 0b0000000001000000000000000000000;
57 +export const GestureLane: Lane = /* */ 0b0000000000000000000000001000000;
58 +
59 +const TransitionHydrationLane: Lane = /* */ 0b0000000000000000000000010000000;
60 +const TransitionLanes: Lanes = /* */ 0b0000000001111111111111100000000;
61 +const TransitionLane1: Lane = /* */ 0b0000000000000000000000100000000;
62 +const TransitionLane2: Lane = /* */ 0b0000000000000000000001000000000;
63 +const TransitionLane3: Lane = /* */ 0b0000000000000000000010000000000;
64 +const TransitionLane4: Lane = /* */ 0b0000000000000000000100000000000;
65 +const TransitionLane5: Lane = /* */ 0b0000000000000000001000000000000;
66 +const TransitionLane6: Lane = /* */ 0b0000000000000000010000000000000;
67 +const TransitionLane7: Lane = /* */ 0b0000000000000000100000000000000;
68 +const TransitionLane8: Lane = /* */ 0b0000000000000001000000000000000;
69 +const TransitionLane9: Lane = /* */ 0b0000000000000010000000000000000;
70 +const TransitionLane10: Lane = /* */ 0b0000000000000100000000000000000;
71 +const TransitionLane11: Lane = /* */ 0b0000000000001000000000000000000;
72 +const TransitionLane12: Lane = /* */ 0b0000000000010000000000000000000;
73 +const TransitionLane13: Lane = /* */ 0b0000000000100000000000000000000;
74 +const TransitionLane14: Lane = /* */ 0b0000000001000000000000000000000;
75
76 const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000;
77 const RetryLane1: Lane = /* */ 0b0000000010000000000000000000000;
@@ -175,6 +176,8 @@ function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
176 return DefaultHydrationLane;
177 case DefaultLane:
178 return DefaultLane;
179 + case GestureLane:
180 + return GestureLane;
181 case TransitionHydrationLane:
182 return TransitionHydrationLane;
183 case TransitionLane1:
@@ -191,7 +194,6 @@ function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
194 case TransitionLane12:
195 case TransitionLane13:
196 case TransitionLane14:
194 - case TransitionLane15:
197 return lanes & TransitionLanes;
198 case RetryLane1:
199 case RetryLane2:
@@ -459,6 +461,7 @@ function computeExpirationTime(lane: Lane, currentTime: number) {
461 case SyncLane:
462 case InputContinuousHydrationLane:
463 case InputContinuousLane:
464 + case GestureLane:
465 // User interactions should expire slightly more quickly.
466 //
467 // NOTE: This is set to the corresponding constant as in Scheduler.js.
@@ -486,7 +489,6 @@ function computeExpirationTime(lane: Lane, currentTime: number) {
489 case TransitionLane12:
490 case TransitionLane13:
491 case TransitionLane14:
489 - case TransitionLane15:
492 return currentTime + transitionLaneExpirationMs;
493 case RetryLane1:
494 case RetryLane2:
@@ -640,7 +642,8 @@ export function includesBlockingLane(lanes: Lanes): boolean {
642 InputContinuousHydrationLane |
643 InputContinuousLane |
644 DefaultHydrationLane |
643 - DefaultLane;
645 + DefaultLane |
646 + GestureLane;
647 return (lanes & SyncDefaultLanes) !== NoLanes;
648 }
649
@@ -663,6 +666,11 @@ export function isTransitionLane(lane: Lane): boolean {
666 return (lane & TransitionLanes) !== NoLanes;
667 }
668
669 +export function isGestureRender(lanes: Lanes): boolean {
670 + // This should render only the one lane.
671 + return lanes === GestureLane;
672 +}
673 +
674 export function claimNextTransitionLane(): Lane {
675 // Cycle through the lanes, assigning each new transition to the next lane.
676 // In most cases, this means every transition gets its own lane, until we
@@ -1053,7 +1061,6 @@ export function getBumpedLaneForHydrationByLane(lane: Lane): Lane {
1061 case TransitionLane12:
1062 case TransitionLane13:
1063 case TransitionLane14:
1056 - case TransitionLane15:
1064 case RetryLane1:
1065 case RetryLane2:
1066 case RetryLane3:
@@ -1197,7 +1204,8 @@ export function getGroupNameOfHighestPriorityLane(lanes: Lanes): string {
1204 InputContinuousHydrationLane |
1205 InputContinuousLane |
1206 DefaultHydrationLane |
1200 - DefaultLane)
1207 + DefaultLane |
1208 + GestureLane)
1209 ) {
1210 return 'Blocking';
1211 }
packages/react-reconciler/src/ReactFiberRoot.js
+5
@@ -33,6 +33,7 @@ import {
33 enableUpdaterTracking,
34 enableTransitionTracing,
35 disableLegacyMode,
36 + enableSwipeTransition,
37 } from 'shared/ReactFeatureFlags';
38 import {initializeUpdateQueue} from './ReactFiberClassUpdateQueue';
39 import {LegacyRoot, ConcurrentRoot} from './ReactRootTags';
@@ -97,6 +98,10 @@ function FiberRootNode(
98
99 this.formState = formState;
100
101 + if (enableSwipeTransition) {
102 + this.gestures = null;
103 + }
104 +
105 this.incompleteTransitions = new Map();
106 if (enableTransitionTracing) {
107 this.transitionCallbacks = null;
packages/react-reconciler/src/ReactFiberRootScheduler.js
+6 -2
@@ -20,6 +20,7 @@ import {
20 enableComponentPerformanceTrack,
21 enableSiblingPrerendering,
22 enableYieldingBeforePassive,
23 + enableSwipeTransition,
24 } from 'shared/ReactFeatureFlags';
25 import {
26 NoLane,
@@ -32,6 +33,7 @@ import {
33 claimNextTransitionLane,
34 getNextLanesToFlushSync,
35 checkIfRootIsPrerendering,
36 + isGestureRender,
37 } from './ReactFiberLane';
38 import {
39 CommitContext,
@@ -211,7 +213,8 @@ function flushSyncWorkAcrossRoots_impl(
213 rootHasPendingCommit,
214 );
215 if (
214 - includesSyncLane(nextLanes) &&
216 + (includesSyncLane(nextLanes) ||
217 + (enableSwipeTransition && isGestureRender(nextLanes))) &&
218 !checkIfRootIsPrerendering(root, nextLanes)
219 ) {
220 // This root has pending sync work. Flush it now.
@@ -296,7 +299,8 @@ function processRootScheduleInMicrotask() {
299 syncTransitionLanes !== NoLanes ||
300 // Common case: we're not treating any extra lanes as synchronous, so we
301 // can just check if the next lanes are sync.
299 - includesSyncLane(nextLanes)
302 + includesSyncLane(nextLanes) ||
303 + (enableSwipeTransition && isGestureRender(nextLanes))
304 ) {
305 mightHavePendingSyncWork = true;
306 }
packages/react-reconciler/src/ReactFiberWorkLoop.js
+44
@@ -47,6 +47,7 @@ import {
47 enableYieldingBeforePassive,
48 enableThrottledScheduling,
49 enableViewTransition,
50 + enableSwipeTransition,
51 } from 'shared/ReactFeatureFlags';
52 import ReactSharedInternals from 'shared/ReactSharedInternals';
53 import is from 'shared/objectIs';
@@ -184,6 +185,8 @@ import {
185 claimNextTransitionLane,
186 checkIfRootIsPrerendering,
187 includesOnlyViewTransitionEligibleLanes,
188 + isGestureRender,
189 + GestureLane,
190 } from './ReactFiberLane';
191 import {
192 DiscreteEventPriority,
@@ -338,6 +341,7 @@ import {
341 import {getMaskedContext, getUnmaskedContext} from './ReactFiberContext';
342 import {peekEntangledActionLane} from './ReactFiberAsyncAction';
343 import {logUncaughtError} from './ReactFiberErrorLogger';
344 +import {deleteScheduledGesture} from './ReactFiberGestureScheduler';
345
346 const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
347
@@ -3287,6 +3291,13 @@ function commitRoot(
3291 const concurrentlyUpdatedLanes = getConcurrentlyUpdatedLanes();
3292 remainingLanes = mergeLanes(remainingLanes, concurrentlyUpdatedLanes);
3293
3294 + if (enableSwipeTransition && root.gestures === null) {
3295 + // Gestures don't clear their lanes while the gesture is still active but it
3296 + // might not be scheduled to do any more renders and so we shouldn't schedule
3297 + // any more gesture lane work until a new gesture is scheduled.
3298 + remainingLanes &= ~GestureLane;
3299 + }
3300 +
3301 markRootFinished(
3302 root,
3303 lanes,
@@ -3310,6 +3321,21 @@ function commitRoot(
3321 // times out.
3322 }
3323
3324 + if (enableSwipeTransition && isGestureRender(lanes)) {
3325 + // This is a special kind of render that doesn't commit regular effects.
3326 + commitGestureOnRoot(
3327 + root,
3328 + finishedWork,
3329 + recoverableErrors,
3330 + enableProfilerTimer
3331 + ? suspendedCommitReason === IMMEDIATE_COMMIT
3332 + ? completedRenderEndTime
3333 + : commitStartTime
3334 + : 0,
3335 + );
3336 + return;
3337 + }
3338 +
3339 // workInProgressX might be overwritten, so we want
3340 // to store it in pendingPassiveX until they get processed
3341 // We need to pass this through as an argument to commitRoot
@@ -3802,6 +3828,24 @@ function flushSpawnedWork(): void {
3828 }
3829 }
3830
3831 +function commitGestureOnRoot(
3832 + root: FiberRoot,
3833 + finishedWork: null | Fiber,
3834 + recoverableErrors: null | Array<CapturedValue<mixed>>,
3835 + renderEndTime: number, // Profiling-only
3836 +): void {
3837 + // We assume that the gesture we just rendered was the first one in the queue.
3838 + const finishedGesture = root.gestures;
3839 + if (finishedGesture === null) {
3840 + throw new Error(
3841 + 'Finished rendering the gesture lane but there were no pending gestures. ' +
3842 + 'React should not have started a render in this case. This is a bug in React.',
3843 + );
3844 + }
3845 + deleteScheduledGesture(root, finishedGesture);
3846 + // TODO: Run the gesture
3847 +}
3848 +
3849 function makeErrorInfo(componentStack: ?string) {
3850 const errorInfo = {
3851 componentStack,
packages/react-reconciler/src/ReactInternalTypes.js
+13 -1
@@ -17,6 +17,7 @@ import type {
17 Awaited,
18 ReactComponentInfo,
19 ReactDebugInfo,
20 + StartGesture,
21 } from 'shared/ReactTypes';
22 import type {WorkTag} from './ReactWorkTags';
23 import type {TypeOfMode} from './ReactTypeOfMode';
@@ -38,6 +39,7 @@ import type {
39 import type {ConcurrentUpdate} from './ReactFiberConcurrentUpdates';
40 import type {ComponentStackNode} from 'react-server/src/ReactFizzComponentStack';
41 import type {ThenableState} from './ReactFiberThenable';
42 +import type {ScheduledGesture} from './ReactFiberGestureScheduler';
43
44 // Unwind Circular: moved from ReactFiberHooks.old
45 export type HookType =
@@ -60,7 +62,8 @@ export type HookType =
62 | 'useCacheRefresh'
63 | 'useOptimistic'
64 | 'useFormState'
63 - | 'useActionState';
65 + | 'useActionState'
66 + | 'useSwipeTransition';
67
68 export type ContextDependency<T> = {
69 context: ReactContext<T>,
@@ -279,6 +282,9 @@ type BaseFiberRootProperties = {
282 ) => void,
283
284 formState: ReactFormState<any, any> | null,
285 +
286 + // enableSwipeTransition only
287 + gestures: null | ScheduledGesture,
288 };
289
290 // The following attributes are only used by DevTools and are only present in DEV builds.
@@ -442,6 +448,12 @@ export type Dispatcher = {
448 initialState: Awaited<S>,
449 permalink?: string,
450 ) => [Awaited<S>, (P) => void, boolean],
451 + // TODO: Non-nullable once `enableSwipeTransition` is on everywhere.
452 + useSwipeTransition?: <T>(
453 + previous: T,
454 + current: T,
455 + next: T,
456 + ) => [T, StartGesture],
457 };
458
459 export type AsyncDispatcher = {
packages/react-server/src/ReactFizzHooks.js
+32 -10
@@ -16,6 +16,7 @@ import type {
16 Usable,
17 ReactCustomFormAction,
18 Awaited,
19 + StartGesture,
20 } from 'shared/ReactTypes';
21
22 import type {ResumableState} from './ReactFizzConfig';
@@ -38,7 +39,10 @@ import {
39 } from './ReactFizzConfig';
40 import {createFastHash} from './ReactServerStreamConfig';
41
41 -import {enableUseEffectEventHook} from 'shared/ReactFeatureFlags';
42 +import {
43 + enableUseEffectEventHook,
44 + enableSwipeTransition,
45 +} from 'shared/ReactFeatureFlags';
46 import is from 'shared/objectIs';
47 import {
48 REACT_CONTEXT_TYPE,
@@ -795,6 +799,19 @@ function useMemoCache(size: number): Array<mixed> {
799 return data;
800 }
801
802 +function unsupportedStartGesture() {
803 + throw new Error('startGesture cannot be called during server rendering.');
804 +}
805 +
806 +function useSwipeTransition<T>(
807 + previous: T,
808 + current: T,
809 + next: T,
810 +): [T, StartGesture] {
811 + resolveCurrentlyRenderingComponent();
812 + return [current, unsupportedStartGesture];
813 +}
814 +
815 function noop(): void {}
816
817 function clientHookNotSupported() {
@@ -837,25 +854,25 @@ export const HooksDispatcher: Dispatcher = supportsClientAPIs
854 : {
855 readContext,
856 use,
857 + useCallback,
858 useContext,
859 + useEffect: clientHookNotSupported,
860 + useImperativeHandle: clientHookNotSupported,
861 + useInsertionEffect: clientHookNotSupported,
862 + useLayoutEffect: clientHookNotSupported,
863 useMemo,
864 useReducer: clientHookNotSupported,
865 useRef: clientHookNotSupported,
866 useState: clientHookNotSupported,
845 - useInsertionEffect: clientHookNotSupported,
846 - useLayoutEffect: clientHookNotSupported,
847 - useCallback,
848 - useImperativeHandle: clientHookNotSupported,
849 - useEffect: clientHookNotSupported,
867 useDebugValue: noop,
868 useDeferredValue: clientHookNotSupported,
869 useTransition: clientHookNotSupported,
853 - useId,
870 useSyncExternalStore: clientHookNotSupported,
855 - useOptimistic,
856 - useActionState,
857 - useFormState: useActionState,
871 + useId,
872 useHostTransitionStatus,
873 + useFormState: useActionState,
874 + useActionState,
875 + useOptimistic,
876 useMemoCache,
877 useCacheRefresh,
878 };
@@ -863,6 +880,11 @@ export const HooksDispatcher: Dispatcher = supportsClientAPIs
880 if (enableUseEffectEventHook) {
881 HooksDispatcher.useEffectEvent = useEffectEvent;
882 }
883 +if (enableSwipeTransition) {
884 + HooksDispatcher.useSwipeTransition = supportsClientAPIs
885 + ? useSwipeTransition
886 + : clientHookNotSupported;
887 +}
888
889 export let currentResumableState: null | ResumableState = (null: any);
890 export function setCurrentResumableState(
packages/react-server/src/ReactFlightHooks.js
+28 -17
@@ -17,6 +17,10 @@ import {
17 } from 'shared/ReactSymbols';
18 import {createThenableState, trackUsedThenable} from './ReactFlightThenable';
19 import {isClientReference} from './ReactFlightServerConfig';
20 +import {
21 + enableUseEffectEventHook,
22 + enableSwipeTransition,
23 +} from 'shared/ReactFeatureFlags';
24
25 let currentRequest = null;
26 let thenableIndexCounter = 0;
@@ -58,33 +62,32 @@ export function getThenableStateAfterSuspending(): ThenableState {
62 }
63
64 export const HooksDispatcher: Dispatcher = {
61 - useMemo<T>(nextCreate: () => T): T {
62 - return nextCreate();
63 - },
65 + readContext: (unsupportedContext: any),
66 +
67 + use,
68 useCallback<T>(callback: T): T {
69 return callback;
70 },
67 - useDebugValue(): void {},
68 - useDeferredValue: (unsupportedHook: any),
69 - useTransition: (unsupportedHook: any),
70 - readContext: (unsupportedContext: any),
71 useContext: (unsupportedContext: any),
72 + useEffect: (unsupportedHook: any),
73 + useImperativeHandle: (unsupportedHook: any),
74 + useLayoutEffect: (unsupportedHook: any),
75 + useInsertionEffect: (unsupportedHook: any),
76 + useMemo<T>(nextCreate: () => T): T {
77 + return nextCreate();
78 + },
79 useReducer: (unsupportedHook: any),
80 useRef: (unsupportedHook: any),
81 useState: (unsupportedHook: any),
75 - useInsertionEffect: (unsupportedHook: any),
76 - useLayoutEffect: (unsupportedHook: any),
77 - useImperativeHandle: (unsupportedHook: any),
78 - useEffect: (unsupportedHook: any),
82 + useDebugValue(): void {},
83 + useDeferredValue: (unsupportedHook: any),
84 + useTransition: (unsupportedHook: any),
85 + useSyncExternalStore: (unsupportedHook: any),
86 useId,
87 useHostTransitionStatus: (unsupportedHook: any),
81 - useOptimistic: (unsupportedHook: any),
88 useFormState: (unsupportedHook: any),
89 useActionState: (unsupportedHook: any),
84 - useSyncExternalStore: (unsupportedHook: any),
85 - useCacheRefresh(): <T>(?() => T, ?T) => void {
86 - return unsupportedRefresh;
87 - },
90 + useOptimistic: (unsupportedHook: any),
91 useMemoCache(size: number): Array<any> {
92 const data = new Array<any>(size);
93 for (let i = 0; i < size; i++) {
@@ -92,8 +95,16 @@ export const HooksDispatcher: Dispatcher = {
95 }
96 return data;
97 },
95 - use,
98 + useCacheRefresh(): <T>(?() => T, ?T) => void {
99 + return unsupportedRefresh;
100 + },
101 };
102 +if (enableUseEffectEventHook) {
103 + HooksDispatcher.useEffectEvent = (unsupportedHook: any);
104 +}
105 +if (enableSwipeTransition) {
106 + HooksDispatcher.useSwipeTransition = (unsupportedHook: any);
107 +}
108
109 function unsupportedHook(): void {
110 throw new Error('This Hook is not supported in Server Components.');
packages/react/index.experimental.development.js
+1
@@ -33,6 +33,7 @@ export {
33 unstable_getCacheForType,
34 unstable_SuspenseList,
35 unstable_ViewTransition,
36 + unstable_useSwipeTransition,
37 unstable_addTransitionType,
38 unstable_useCacheRefresh,
39 useId,
packages/react/index.experimental.js
+1
@@ -33,6 +33,7 @@ export {
33 unstable_getCacheForType,
34 unstable_SuspenseList,
35 unstable_ViewTransition,
36 + unstable_useSwipeTransition,
37 unstable_addTransitionType,
38 unstable_useCacheRefresh,
39 useId,
packages/react/src/ReactClient.js
+6 -2
@@ -57,6 +57,7 @@ import {
57 use,
58 useOptimistic,
59 useActionState,
60 + useSwipeTransition,
61 } from './ReactHooks';
62 import ReactSharedInternals from './ReactSharedInternalsClient';
63 import {startTransition} from './ReactStartTransition';
@@ -126,7 +127,10 @@ export {
127 // enableViewTransition
128 REACT_VIEW_TRANSITION_TYPE as unstable_ViewTransition,
129 addTransitionType as unstable_addTransitionType,
130 + // enableSwipeTransition
131 + useSwipeTransition as unstable_useSwipeTransition,
132 + // DEV-only
133 useId,
130 - act, // DEV-only
131 - captureOwnerStack, // DEV-only
134 + act,
135 + captureOwnerStack,
136 };
packages/react/src/ReactHooks.js
+18 -1
@@ -13,12 +13,16 @@ import type {
13 StartTransitionOptions,
14 Usable,
15 Awaited,
16 + StartGesture,
17 } from 'shared/ReactTypes';
18 import {REACT_CONSUMER_TYPE} from 'shared/ReactSymbols';
19
20 import ReactSharedInternals from 'shared/ReactSharedInternals';
21
21 -import {enableUseEffectCRUDOverload} from 'shared/ReactFeatureFlags';
22 +import {
23 + enableUseEffectCRUDOverload,
24 + enableSwipeTransition,
25 +} from 'shared/ReactFeatureFlags';
26
27 type BasicStateAction<S> = (S => S) | S;
28 type Dispatch<A> = A => void;
@@ -261,3 +265,16 @@ export function useActionState<S, P>(
265 const dispatcher = resolveDispatcher();
266 return dispatcher.useActionState(action, initialState, permalink);
267 }
268 +
269 +export function useSwipeTransition<T>(
270 + previous: T,
271 + current: T,
272 + next: T,
273 +): [T, StartGesture] {
274 + if (!enableSwipeTransition) {
275 + throw new Error('Not implemented.');
276 + }
277 + const dispatcher = resolveDispatcher();
278 + // $FlowFixMe[not-a-function] This is unstable, thus optional
279 + return dispatcher.useSwipeTransition(previous, current, next);
280 +}
packages/shared/ReactFeatureFlags.js
+2
@@ -92,6 +92,8 @@ export const enableHalt = __EXPERIMENTAL__;
92
93 export const enableViewTransition = __EXPERIMENTAL__;
94
95 +export const enableSwipeTransition = __EXPERIMENTAL__;
96 +
97 /**
98 * Switches Fiber creation to a simple object instead of a constructor.
99 */
packages/shared/ReactTypes.js
+6
@@ -168,6 +168,12 @@ export type ReactFormState<S, ReferenceId> = [
168 number /* number of bound arguments */,
169 ];
170
171 +// Intrinsic GestureProvider. This type varies by Environment whether a particular
172 +// renderer supports it.
173 +export type GestureProvider = AnimationTimeline; // TODO: More provider types.
174 +
175 +export type StartGesture = (gestureProvider: GestureProvider) => () => void;
176 +
177 export type Awaited<T> = T extends null | void
178 ? T // special case for `null | undefined` when not in `--strictNullChecks` mode
179 : T extends Object // `await` only unwraps object types with a callable then. Non-object types are not unwrapped.
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -82,6 +82,7 @@ export const enableHydrationLaneScheduling = true;
82 export const enableYieldingBeforePassive = false;
83 export const enableThrottledScheduling = false;
84 export const enableViewTransition = false;
85 +export const enableSwipeTransition = false;
86
87 // Flow magic to verify the exports of this file match the original version.
88 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -71,6 +71,7 @@ export const enableYieldingBeforePassive = false;
71
72 export const enableThrottledScheduling = false;
73 export const enableViewTransition = false;
74 +export const enableSwipeTransition = false;
75 export const enableFastAddPropertiesInDiffing = false;
76 export const enableLazyPublicInstanceInFabric = false;
77
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -70,6 +70,7 @@ export const enableYieldingBeforePassive = true;
70
71 export const enableThrottledScheduling = false;
72 export const enableViewTransition = false;
73 +export const enableSwipeTransition = false;
74 export const enableFastAddPropertiesInDiffing = true;
75 export const enableLazyPublicInstanceInFabric = false;
76
packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js
+1
@@ -67,6 +67,7 @@ export const enableHydrationLaneScheduling = true;
67 export const enableYieldingBeforePassive = false;
68 export const enableThrottledScheduling = false;
69 export const enableViewTransition = false;
70 +export const enableSwipeTransition = false;
71 export const enableRemoveConsolePatches = false;
72 export const enableFastAddPropertiesInDiffing = false;
73 export const enableLazyPublicInstanceInFabric = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -82,6 +82,7 @@ export const enableYieldingBeforePassive = false;
82
83 export const enableThrottledScheduling = false;
84 export const enableViewTransition = false;
85 +export const enableSwipeTransition = false;
86 export const enableRemoveConsolePatches = false;
87 export const enableFastAddPropertiesInDiffing = false;
88 export const enableLazyPublicInstanceInFabric = false;
packages/shared/forks/ReactFeatureFlags.www.js
+2
@@ -112,5 +112,7 @@ export const enableShallowPropDiffing = false;
112
113 export const enableLazyPublicInstanceInFabric = false;
114
115 +export const enableSwipeTransition = false;
116 +
117 // Flow magic to verify the exports of this file match the original version.
118 ((((null: any): ExportsType): FeatureFlagsType): ExportsType);
scripts/error-codes/codes.json
+3 -1
@@ -531,5 +531,7 @@
531 "543": "Expected a ResourceEffectUpdate to be pushed together with ResourceEffectIdentity. This is a bug in React.",
532 "544": "Found a pair with an auto name. This is a bug in React.",
533 "545": "The %s tag may only be rendered once.",
534 - "546": "useEffect CRUD overload is not enabled in this build of React."
534 + "546": "useEffect CRUD overload is not enabled in this build of React.",
535 + "547": "startGesture cannot be called during server rendering.",
536 + "548": "Finished rendering the gesture lane but there were no pending gestures. React should not have started a render in this case. This is a bug in React."
537 }