main
js 205 lines 6.57 KB
Raw
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 {Fiber} from 'react-reconciler/src/ReactInternalTypes';
11 import type {
12 StartTransitionOptions,
13 GestureProvider,
14 GestureOptions,
15 } from 'shared/ReactTypes';
16 import type {TransitionTypes} from './ReactTransitionType';
17
18 import ReactSharedInternals from 'shared/ReactSharedInternals';
19
20 import {
21 enableTransitionTracing,
22 enableViewTransition,
23 enableGestureTransition,
24 } from 'shared/ReactFeatureFlags';
25
26 import reportGlobalError from 'shared/reportGlobalError';
27
28 import noop from 'shared/noop';
29
30 export type Transition = {
31 types: null | TransitionTypes, // enableViewTransition
32 gesture: null | GestureProvider, // enableGestureTransition
33 name: null | string, // enableTransitionTracing only
34 startTime: number, // enableTransitionTracing only
35 _updatedFibers: Set<Fiber>, // DEV-only
36 ...
37 };
38
39 function releaseAsyncTransition() {
40 if (__DEV__) {
41 ReactSharedInternals.asyncTransitions--;
42 }
43 }
44
45 export function startTransition(
46 scope: () => void,
47 options?: StartTransitionOptions,
48 ): void {
49 const prevTransition = ReactSharedInternals.T;
50 const currentTransition: Transition = {} as any;
51 if (enableViewTransition) {
52 currentTransition.types =
53 prevTransition !== null
54 ? // If we're a nested transition, we should use the same set as the parent
55 // since we're conceptually always joined into the same entangled transition.
56 // In practice, this only matters if we add transition types in the inner
57 // without setting state. In that case, the inner transition can finish
58 // without waiting for the outer.
59 prevTransition.types
60 : null;
61 }
62 if (enableGestureTransition) {
63 currentTransition.gesture = null;
64 }
65 if (enableTransitionTracing) {
66 currentTransition.name =
67 options !== undefined && options.name !== undefined ? options.name : null;
68 currentTransition.startTime = -1; // TODO: This should read the timestamp.
69 }
70 if (__DEV__) {
71 currentTransition._updatedFibers = new Set();
72 }
73 ReactSharedInternals.T = currentTransition;
74
75 try {
76 const returnValue = scope();
77 const onStartTransitionFinish = ReactSharedInternals.S;
78 if (onStartTransitionFinish !== null) {
79 onStartTransitionFinish(currentTransition, returnValue);
80 }
81 if (
82 typeof returnValue === 'object' &&
83 // $FlowFixMe[invalid-compare]
84 returnValue !== null &&
85 typeof returnValue.then === 'function'
86 ) {
87 if (__DEV__) {
88 // Keep track of the number of async transitions still running so we can warn.
89 ReactSharedInternals.asyncTransitions++;
90 returnValue.then(releaseAsyncTransition, releaseAsyncTransition);
91 }
92 returnValue.then(noop, reportGlobalError);
93 }
94 } catch (error) {
95 reportGlobalError(error);
96 } finally {
97 warnAboutTransitionSubscriptions(prevTransition, currentTransition);
98 if (prevTransition !== null && currentTransition.types !== null) {
99 // If we created a new types set in the inner transition, we transfer it to the parent
100 // since they should share the same set. They're conceptually entangled.
101 if (__DEV__) {
102 if (
103 prevTransition.types !== null &&
104 prevTransition.types !== currentTransition.types
105 ) {
106 // Just assert that assumption holds that we're not overriding anything.
107 console.error(
108 'We expected inner Transitions to have transferred the outer types set and ' +
109 'that you cannot add to the outer Transition while inside the inner.' +
110 'This is a bug in React.',
111 );
112 }
113 }
114 prevTransition.types = currentTransition.types;
115 }
116 ReactSharedInternals.T = prevTransition;
117 }
118 }
119
120 export function startGestureTransition(
121 provider: GestureProvider,
122 scope: () => void,
123 options?: GestureOptions & StartTransitionOptions,
124 ): () => void {
125 if (!enableGestureTransition) {
126 // eslint-disable-next-line react-internal/prod-error-codes
127 throw new Error(
128 'startGestureTransition should not be exported when the enableGestureTransition flag is off.',
129 );
130 }
131 if (provider == null) {
132 // We enforce this at runtime even though the type also enforces it since we
133 // use null as a signal internally so it would lead it to be treated as a
134 // regular transition otherwise.
135 throw new Error(
136 'A Timeline is required as the first argument to startGestureTransition.',
137 );
138 }
139 const prevTransition = ReactSharedInternals.T;
140 const currentTransition: Transition = {} as any;
141 if (enableViewTransition) {
142 currentTransition.types = null;
143 }
144 // $FlowFixMe[constant-condition]
145 if (enableGestureTransition) {
146 currentTransition.gesture = provider;
147 }
148 if (enableTransitionTracing) {
149 currentTransition.name =
150 options !== undefined && options.name !== undefined ? options.name : null;
151 currentTransition.startTime = -1; // TODO: This should read the timestamp.
152 }
153 if (__DEV__) {
154 currentTransition._updatedFibers = new Set();
155 }
156 ReactSharedInternals.T = currentTransition;
157
158 try {
159 const returnValue = scope();
160 if (__DEV__) {
161 if (
162 typeof returnValue === 'object' &&
163 // $FlowFixMe[invalid-compare]
164 returnValue !== null &&
165 typeof returnValue.then === 'function'
166 ) {
167 console.error(
168 'Cannot use an async function in startGestureTransition. It must be able to start immediately.',
169 );
170 }
171 }
172 const onStartGestureTransitionFinish = ReactSharedInternals.G;
173 if (onStartGestureTransitionFinish !== null) {
174 return onStartGestureTransitionFinish(
175 currentTransition,
176 provider,
177 options,
178 );
179 }
180 } catch (error) {
181 reportGlobalError(error);
182 } finally {
183 ReactSharedInternals.T = prevTransition;
184 }
185 return noop;
186 }
187
188 function warnAboutTransitionSubscriptions(
189 prevTransition: Transition | null,
190 currentTransition: Transition,
191 ) {
192 if (__DEV__) {
193 if (prevTransition === null && currentTransition._updatedFibers) {
194 const updatedFibersCount = currentTransition._updatedFibers.size;
195 currentTransition._updatedFibers.clear();
196 if (updatedFibersCount > 10) {
197 console.warn(
198 'Detected a large number of updates inside startTransition. ' +
199 'If this is due to a subscription please re-write it to use React provided hooks. ' +
200 'Otherwise concurrent mode guarantees are off the table.',
201 );
202 }
203 }
204 }
205 }