| 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 {Thenable} from 'shared/ReactTypes'; |
| 11 | import type {RendererTask} from './ReactSharedInternalsClient'; |
| 12 | import ReactSharedInternals from './ReactSharedInternalsClient'; |
| 13 | import queueMacrotask from 'shared/enqueueTask'; |
| 14 | |
| 15 | import {disableLegacyMode} from 'shared/ReactFeatureFlags'; |
| 16 | |
| 17 | // `act` calls can be nested, so we track the depth. This represents the |
| 18 | // number of `act` scopes on the stack. |
| 19 | let actScopeDepth = 0; |
| 20 | |
| 21 | // We only warn the first time you neglect to await an async `act` scope. |
| 22 | let didWarnNoAwaitAct = false; |
| 23 | |
| 24 | function aggregateErrors(errors: Array<mixed>): mixed { |
| 25 | if (errors.length > 1 && typeof AggregateError === 'function') { |
| 26 | return new AggregateError(errors); |
| 27 | } |
| 28 | return errors[0]; |
| 29 | } |
| 30 | |
| 31 | export function act<T>(callback: () => T | Thenable<T>): Thenable<T> { |
| 32 | if (__DEV__) { |
| 33 | // When ReactSharedInternals.actQueue is not null, it signals to React that |
| 34 | // we're currently inside an `act` scope. React will push all its tasks to |
| 35 | // this queue instead of scheduling them with platform APIs. |
| 36 | // |
| 37 | // We set this to an empty array when we first enter an `act` scope, and |
| 38 | // only unset it once we've left the outermost `act` scope — remember that |
| 39 | // `act` calls can be nested. |
| 40 | // |
| 41 | // If we're already inside an `act` scope, reuse the existing queue. |
| 42 | const prevIsBatchingLegacy = !disableLegacyMode |
| 43 | ? ReactSharedInternals.isBatchingLegacy |
| 44 | : false; |
| 45 | const prevActQueue = ReactSharedInternals.actQueue; |
| 46 | const prevActScopeDepth = actScopeDepth; |
| 47 | actScopeDepth++; |
| 48 | const queue = (ReactSharedInternals.actQueue = |
| 49 | prevActQueue !== null ? prevActQueue : []); |
| 50 | // Used to reproduce behavior of `batchedUpdates` in legacy mode. Only |
| 51 | // set to `true` while the given callback is executed, not for updates |
| 52 | // triggered during an async event, because this is how the legacy |
| 53 | // implementation of `act` behaved. |
| 54 | if (!disableLegacyMode) { |
| 55 | ReactSharedInternals.isBatchingLegacy = true; |
| 56 | } |
| 57 | |
| 58 | let result; |
| 59 | // This tracks whether the `act` call is awaited. In certain cases, not |
| 60 | // awaiting it is a mistake, so we will detect that and warn. |
| 61 | let didAwaitActCall = false; |
| 62 | try { |
| 63 | // Reset this to `false` right before entering the React work loop. The |
| 64 | // only place we ever read this fields is just below, right after running |
| 65 | // the callback. So we don't need to reset after the callback runs. |
| 66 | if (!disableLegacyMode) { |
| 67 | ReactSharedInternals.didScheduleLegacyUpdate = false; |
| 68 | } |
| 69 | result = callback(); |
| 70 | const didScheduleLegacyUpdate = !disableLegacyMode |
| 71 | ? ReactSharedInternals.didScheduleLegacyUpdate |
| 72 | : false; |
| 73 | |
| 74 | // Replicate behavior of original `act` implementation in legacy mode, |
| 75 | // which flushed updates immediately after the scope function exits, even |
| 76 | // if it's an async function. |
| 77 | if (!prevIsBatchingLegacy && didScheduleLegacyUpdate) { |
| 78 | flushActQueue(queue); |
| 79 | } |
| 80 | // `isBatchingLegacy` gets reset using the regular stack, not the async |
| 81 | // one used to track `act` scopes. Why, you may be wondering? Because |
| 82 | // that's how it worked before version 18. Yes, it's confusing! We should |
| 83 | // delete legacy mode!! |
| 84 | if (!disableLegacyMode) { |
| 85 | ReactSharedInternals.isBatchingLegacy = prevIsBatchingLegacy; |
| 86 | } |
| 87 | } catch (error) { |
| 88 | // `isBatchingLegacy` gets reset using the regular stack, not the async |
| 89 | // one used to track `act` scopes. Why, you may be wondering? Because |
| 90 | // that's how it worked before version 18. Yes, it's confusing! We should |
| 91 | // delete legacy mode!! |
| 92 | ReactSharedInternals.thrownErrors.push(error); |
| 93 | } |
| 94 | if (ReactSharedInternals.thrownErrors.length > 0) { |
| 95 | if (!disableLegacyMode) { |
| 96 | ReactSharedInternals.isBatchingLegacy = prevIsBatchingLegacy; |
| 97 | } |
| 98 | popActScope(prevActQueue, prevActScopeDepth); |
| 99 | const thrownError = aggregateErrors(ReactSharedInternals.thrownErrors); |
| 100 | ReactSharedInternals.thrownErrors.length = 0; |
| 101 | throw thrownError; |
| 102 | } |
| 103 | |
| 104 | if ( |
| 105 | // $FlowFixMe[invalid-compare] |
| 106 | result !== null && |
| 107 | typeof result === 'object' && |
| 108 | // $FlowFixMe[method-unbinding] |
| 109 | typeof result.then === 'function' |
| 110 | ) { |
| 111 | // A promise/thenable was returned from the callback. Wait for it to |
| 112 | // resolve before flushing the queue. |
| 113 | // |
| 114 | // If `act` were implemented as an async function, this whole block could |
| 115 | // be a single `await` call. That's really the only difference between |
| 116 | // this branch and the next one. |
| 117 | const thenable = result as any as Thenable<T>; |
| 118 | |
| 119 | // Warn if the an `act` call with an async scope is not awaited. In a |
| 120 | // future release, consider making this an error. |
| 121 | queueSeveralMicrotasks(() => { |
| 122 | if (!didAwaitActCall && !didWarnNoAwaitAct) { |
| 123 | didWarnNoAwaitAct = true; |
| 124 | console.error( |
| 125 | 'You called act(async () => ...) without await. ' + |
| 126 | 'This could lead to unexpected testing behaviour, ' + |
| 127 | 'interleaving multiple act calls and mixing their ' + |
| 128 | 'scopes. ' + |
| 129 | 'You should - await act(async () => ...);', |
| 130 | ); |
| 131 | } |
| 132 | }); |
| 133 | |
| 134 | return { |
| 135 | then(resolve: T => mixed, reject: mixed => mixed) { |
| 136 | didAwaitActCall = true; |
| 137 | thenable.then( |
| 138 | returnValue => { |
| 139 | popActScope(prevActQueue, prevActScopeDepth); |
| 140 | if (prevActScopeDepth === 0) { |
| 141 | // We're exiting the outermost `act` scope. Flush the queue. |
| 142 | try { |
| 143 | flushActQueue(queue); |
| 144 | queueMacrotask(() => |
| 145 | // Recursively flush tasks scheduled by a microtask. |
| 146 | recursivelyFlushAsyncActWork(returnValue, resolve, reject), |
| 147 | ); |
| 148 | } catch (error) { |
| 149 | // `thenable` might not be a real promise, and `flushActQueue` |
| 150 | // might throw, so we need to wrap `flushActQueue` in a |
| 151 | // try/catch. |
| 152 | ReactSharedInternals.thrownErrors.push(error); |
| 153 | } |
| 154 | if (ReactSharedInternals.thrownErrors.length > 0) { |
| 155 | const thrownError = aggregateErrors( |
| 156 | ReactSharedInternals.thrownErrors, |
| 157 | ); |
| 158 | ReactSharedInternals.thrownErrors.length = 0; |
| 159 | reject(thrownError); |
| 160 | } |
| 161 | } else { |
| 162 | resolve(returnValue); |
| 163 | } |
| 164 | }, |
| 165 | error => { |
| 166 | popActScope(prevActQueue, prevActScopeDepth); |
| 167 | if (ReactSharedInternals.thrownErrors.length > 0) { |
| 168 | const thrownError = aggregateErrors( |
| 169 | ReactSharedInternals.thrownErrors, |
| 170 | ); |
| 171 | ReactSharedInternals.thrownErrors.length = 0; |
| 172 | reject(thrownError); |
| 173 | } else { |
| 174 | reject(error); |
| 175 | } |
| 176 | }, |
| 177 | ); |
| 178 | }, |
| 179 | }; |
| 180 | } else { |
| 181 | const returnValue: T = result as any; |
| 182 | // The callback is not an async function. Exit the current |
| 183 | // scope immediately. |
| 184 | popActScope(prevActQueue, prevActScopeDepth); |
| 185 | if (prevActScopeDepth === 0) { |
| 186 | // We're exiting the outermost `act` scope. Flush the queue. |
| 187 | flushActQueue(queue); |
| 188 | |
| 189 | // If the queue is not empty, it implies that we intentionally yielded |
| 190 | // to the main thread, because something suspended. We will continue |
| 191 | // in an asynchronous task. |
| 192 | // |
| 193 | // Warn if something suspends but the `act` call is not awaited. |
| 194 | // In a future release, consider making this an error. |
| 195 | if (queue.length !== 0) { |
| 196 | queueSeveralMicrotasks(() => { |
| 197 | if (!didAwaitActCall && !didWarnNoAwaitAct) { |
| 198 | didWarnNoAwaitAct = true; |
| 199 | console.error( |
| 200 | 'A component suspended inside an `act` scope, but the ' + |
| 201 | '`act` call was not awaited. When testing React ' + |
| 202 | 'components that depend on asynchronous data, you must ' + |
| 203 | 'await the result:\n\n' + |
| 204 | 'await act(() => ...)', |
| 205 | ); |
| 206 | } |
| 207 | }); |
| 208 | } |
| 209 | |
| 210 | // Like many things in this module, this is next part is confusing. |
| 211 | // |
| 212 | // We do not currently require every `act` call that is passed a |
| 213 | // callback to be awaited, through arguably we should. Since this |
| 214 | // callback was synchronous, we need to exit the current scope before |
| 215 | // returning. |
| 216 | // |
| 217 | // However, if thenable we're about to return *is* awaited, we'll |
| 218 | // immediately restore the current scope. So it shouldn't observable. |
| 219 | // |
| 220 | // This doesn't affect the case where the scope callback is async, |
| 221 | // because we always require those calls to be awaited. |
| 222 | // |
| 223 | // TODO: In a future version, consider always requiring all `act` calls |
| 224 | // to be awaited, regardless of whether the callback is sync or async. |
| 225 | ReactSharedInternals.actQueue = null; |
| 226 | } |
| 227 | |
| 228 | if (ReactSharedInternals.thrownErrors.length > 0) { |
| 229 | const thrownError = aggregateErrors(ReactSharedInternals.thrownErrors); |
| 230 | ReactSharedInternals.thrownErrors.length = 0; |
| 231 | throw thrownError; |
| 232 | } |
| 233 | |
| 234 | return { |
| 235 | then(resolve: T => mixed, reject: mixed => mixed) { |
| 236 | didAwaitActCall = true; |
| 237 | if (prevActScopeDepth === 0) { |
| 238 | // If the `act` call is awaited, restore the queue we were |
| 239 | // using before (see long comment above) so we can flush it. |
| 240 | ReactSharedInternals.actQueue = queue; |
| 241 | queueMacrotask(() => |
| 242 | // Recursively flush tasks scheduled by a microtask. |
| 243 | recursivelyFlushAsyncActWork(returnValue, resolve, reject), |
| 244 | ); |
| 245 | } else { |
| 246 | resolve(returnValue); |
| 247 | } |
| 248 | }, |
| 249 | }; |
| 250 | } |
| 251 | } else { |
| 252 | throw new Error('act(...) is not supported in production builds of React.'); |
| 253 | } |
| 254 | } |
| 255 | |
| 256 | function popActScope( |
| 257 | prevActQueue: null | Array<RendererTask>, |
| 258 | prevActScopeDepth: number, |
| 259 | ) { |
| 260 | if (__DEV__) { |
| 261 | if (prevActScopeDepth !== actScopeDepth - 1) { |
| 262 | console.error( |
| 263 | 'You seem to have overlapping act() calls, this is not supported. ' + |
| 264 | 'Be sure to await previous act() calls before making a new one. ', |
| 265 | ); |
| 266 | } |
| 267 | actScopeDepth = prevActScopeDepth; |
| 268 | } |
| 269 | } |
| 270 | |
| 271 | function recursivelyFlushAsyncActWork<T>( |
| 272 | returnValue: T, |
| 273 | resolve: T => mixed, |
| 274 | reject: mixed => mixed, |
| 275 | ) { |
| 276 | if (__DEV__) { |
| 277 | // Check if any tasks were scheduled asynchronously. |
| 278 | const queue = ReactSharedInternals.actQueue; |
| 279 | if (queue !== null) { |
| 280 | if (queue.length !== 0) { |
| 281 | // Async tasks were scheduled, mostly likely in a microtask. |
| 282 | // Keep flushing until there are no more. |
| 283 | try { |
| 284 | flushActQueue(queue); |
| 285 | // The work we just performed may have schedule additional async |
| 286 | // tasks. Wait a macrotask and check again. |
| 287 | queueMacrotask(() => |
| 288 | recursivelyFlushAsyncActWork(returnValue, resolve, reject), |
| 289 | ); |
| 290 | return; |
| 291 | } catch (error) { |
| 292 | // Leave remaining tasks on the queue if something throws. |
| 293 | ReactSharedInternals.thrownErrors.push(error); |
| 294 | } |
| 295 | } else { |
| 296 | // The queue is empty. We can finish. |
| 297 | ReactSharedInternals.actQueue = null; |
| 298 | } |
| 299 | } |
| 300 | if (ReactSharedInternals.thrownErrors.length > 0) { |
| 301 | const thrownError = aggregateErrors(ReactSharedInternals.thrownErrors); |
| 302 | ReactSharedInternals.thrownErrors.length = 0; |
| 303 | reject(thrownError); |
| 304 | } else { |
| 305 | resolve(returnValue); |
| 306 | } |
| 307 | } |
| 308 | } |
| 309 | |
| 310 | let isFlushing = false; |
| 311 | function flushActQueue(queue: Array<RendererTask>) { |
| 312 | if (__DEV__) { |
| 313 | if (!isFlushing) { |
| 314 | // Prevent re-entrance. |
| 315 | isFlushing = true; |
| 316 | let i = 0; |
| 317 | try { |
| 318 | for (; i < queue.length; i++) { |
| 319 | let callback: RendererTask = queue[i]; |
| 320 | do { |
| 321 | ReactSharedInternals.didUsePromise = false; |
| 322 | const continuation = callback(false); |
| 323 | if (continuation !== null) { |
| 324 | if (ReactSharedInternals.didUsePromise) { |
| 325 | // The component just suspended. Yield to the main thread in |
| 326 | // case the promise is already resolved. If so, it will ping in |
| 327 | // a microtask and we can resume without unwinding the stack. |
| 328 | queue[i] = callback; |
| 329 | queue.splice(0, i); |
| 330 | return; |
| 331 | } |
| 332 | callback = continuation; |
| 333 | } else { |
| 334 | break; |
| 335 | } |
| 336 | } while (true); |
| 337 | } |
| 338 | // We flushed the entire queue. |
| 339 | queue.length = 0; |
| 340 | } catch (error) { |
| 341 | // If something throws, leave the remaining callbacks on the queue. |
| 342 | queue.splice(0, i + 1); |
| 343 | ReactSharedInternals.thrownErrors.push(error); |
| 344 | } finally { |
| 345 | isFlushing = false; |
| 346 | } |
| 347 | } |
| 348 | } |
| 349 | } |
| 350 | |
| 351 | // Some of our warnings attempt to detect if the `act` call is awaited by |
| 352 | // checking in an asynchronous task. Wait a few microtasks before checking. The |
| 353 | // only reason one isn't sufficient is we want to accommodate the case where an |
| 354 | // `act` call is returned from an async function without first being awaited, |
| 355 | // since that's a somewhat common pattern. If you do this too many times in a |
| 356 | // nested sequence, you might get a warning, but you can always fix by awaiting |
| 357 | // the call. |
| 358 | // |
| 359 | // A macrotask would also work (and is the fallback) but depending on the test |
| 360 | // environment it may cause the warning to fire too late. |
| 361 | const queueSeveralMicrotasks = |
| 362 | typeof queueMicrotask === 'function' |
| 363 | ? (callback: () => void) => { |
| 364 | queueMicrotask(() => queueMicrotask(callback)); |
| 365 | } |
| 366 | : queueMacrotask; |