| 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 | } |