| 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 {Dispatcher} from 'react-reconciler/src/ReactInternalTypes'; |
| 11 | |
| 12 | import type { |
| 13 | ReactContext, |
| 14 | StartTransitionOptions, |
| 15 | Thenable, |
| 16 | Usable, |
| 17 | ReactRecoverable, |
| 18 | ReactCustomFormAction, |
| 19 | Awaited, |
| 20 | } from 'shared/ReactTypes'; |
| 21 | |
| 22 | import type {ResumableState} from './ReactFizzConfig'; |
| 23 | import type {Request, Task, KeyNode} from './ReactFizzServer'; |
| 24 | import type {ThenableState} from './ReactFizzThenable'; |
| 25 | import type {TransitionStatus} from './ReactFizzConfig'; |
| 26 | |
| 27 | import {readContext as readContextImpl} from './ReactFizzNewContext'; |
| 28 | import {getTreeId} from './ReactFizzTreeContext'; |
| 29 | import { |
| 30 | createThenableState, |
| 31 | trackUsedThenable, |
| 32 | readPreviousThenable, |
| 33 | } from './ReactFizzThenable'; |
| 34 | |
| 35 | import { |
| 36 | makeId, |
| 37 | NotPendingTransition, |
| 38 | supportsClientAPIs, |
| 39 | } from './ReactFizzConfig'; |
| 40 | import {createFastHash} from './ReactServerStreamConfig'; |
| 41 | |
| 42 | import is from 'shared/objectIs'; |
| 43 | import hasOwnProperty from 'shared/hasOwnProperty'; |
| 44 | import { |
| 45 | REACT_CONTEXT_TYPE, |
| 46 | REACT_RECOVERABLE_TYPE, |
| 47 | REACT_MEMO_CACHE_SENTINEL, |
| 48 | } from 'shared/ReactSymbols'; |
| 49 | import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion'; |
| 50 | import {getFormState} from './ReactFizzServer'; |
| 51 | |
| 52 | import noop from 'shared/noop'; |
| 53 | |
| 54 | type BasicStateAction<S> = (S => S) | S; |
| 55 | type Dispatch<A> = A => void; |
| 56 | |
| 57 | type Update<A> = { |
| 58 | action: A, |
| 59 | next: Update<A> | null, |
| 60 | }; |
| 61 | |
| 62 | type UpdateQueue<A> = { |
| 63 | last: Update<A> | null, |
| 64 | dispatch: any, |
| 65 | }; |
| 66 | |
| 67 | type Hook = { |
| 68 | memoizedState: any, |
| 69 | queue: UpdateQueue<any> | null, |
| 70 | next: Hook | null, |
| 71 | }; |
| 72 | |
| 73 | let currentlyRenderingComponent: Object | null = null; |
| 74 | let currentlyRenderingTask: Task | null = null; |
| 75 | let currentlyRenderingRequest: Request | null = null; |
| 76 | let currentlyRenderingKeyPath: KeyNode | null = null; |
| 77 | let firstWorkInProgressHook: Hook | null = null; |
| 78 | let workInProgressHook: Hook | null = null; |
| 79 | // Whether the work-in-progress hook is a re-rendered hook |
| 80 | let isReRender: boolean = false; |
| 81 | // Whether an update was scheduled during the currently executing render pass. |
| 82 | let didScheduleRenderPhaseUpdate: boolean = false; |
| 83 | // Counts the number of useId hooks in this component |
| 84 | let localIdCounter: number = 0; |
| 85 | // Chunks that should be pushed to the stream once the component |
| 86 | // finishes rendering. |
| 87 | // Counts the number of useActionState calls in this component |
| 88 | let actionStateCounter: number = 0; |
| 89 | // The index of the useActionState hook that matches the one passed in at the |
| 90 | // root during an MPA navigation, if any. |
| 91 | let actionStateMatchingIndex: number = -1; |
| 92 | // Counts the number of use(thenable) calls in this component |
| 93 | let thenableIndexCounter: number = 0; |
| 94 | let thenableState: ThenableState | null = null; |
| 95 | |
| 96 | const browserReasonInitializationFallback = |
| 97 | 'The reason for browser-only rendering could not be determined because its ' + |
| 98 | 'initializer threw.'; |
| 99 | |
| 100 | export function createRecoverableError(recoverable: ReactRecoverable): Error { |
| 101 | const reason = recoverable._reason; |
| 102 | let initializedReason; |
| 103 | if (typeof reason === 'function') { |
| 104 | try { |
| 105 | initializedReason = reason(); |
| 106 | } catch { |
| 107 | // A reason is only diagnostic metadata. Its initializer must not affect |
| 108 | // whether the renderer can defer this subtree to the browser. |
| 109 | initializedReason = browserReasonInitializationFallback; |
| 110 | } |
| 111 | } else { |
| 112 | initializedReason = reason; |
| 113 | } |
| 114 | // Always create the recoverable at the consumption point so its stack |
| 115 | // identifies the relevant use() or abort() call. A lazy reason is diagnostic |
| 116 | // metadata and can be any value supported by Error.cause. |
| 117 | const error = new Error( |
| 118 | 'Browser-only rendering was requested by `browser()`.', |
| 119 | reason === undefined ? undefined : {cause: initializedReason}, |
| 120 | ); |
| 121 | Object.defineProperty(error, REACT_RECOVERABLE_TYPE, {value: true}); |
| 122 | return error; |
| 123 | } |
| 124 | |
| 125 | export function isRecoverableError(error: mixed): boolean { |
| 126 | if (typeof error !== 'object' || error === null) { |
| 127 | return false; |
| 128 | } |
| 129 | return (error as any)[REACT_RECOVERABLE_TYPE] === true; |
| 130 | } |
| 131 | |
| 132 | export function cloneRecoverableErrorAsFatal(recoverableError: Error): Error { |
| 133 | // Create a separate diagnostic for fatal reporting without changing the |
| 134 | // branded recoverable error that other tasks may still need to observe. |
| 135 | const fatalRecoverableError = new Error( |
| 136 | 'The server render could not complete because client rendering was ' + |
| 137 | "requested outside a Suspense boundary. See this error's cause for " + |
| 138 | 'additional details.', |
| 139 | hasOwnProperty.call(recoverableError, 'cause') |
| 140 | ? {cause: (recoverableError as any).cause} |
| 141 | : undefined, |
| 142 | ); |
| 143 | // Keep the frames captured where the recoverable was consumed, but replace |
| 144 | // the first line with the fatal error's message. |
| 145 | const stack = recoverableError.stack; |
| 146 | if (stack !== undefined) { |
| 147 | const frameStart = stack.indexOf('\n'); |
| 148 | fatalRecoverableError.stack = |
| 149 | fatalRecoverableError.name + |
| 150 | ': ' + |
| 151 | fatalRecoverableError.message + |
| 152 | (frameStart === -1 ? '' : stack.slice(frameStart)); |
| 153 | } else { |
| 154 | (fatalRecoverableError as any).stack = undefined; |
| 155 | } |
| 156 | return fatalRecoverableError; |
| 157 | } |
| 158 | |
| 159 | // Lazily created map of render-phase updates |
| 160 | let renderPhaseUpdates: Map<UpdateQueue<any>, Update<any>> | null = null; |
| 161 | // Counter to prevent infinite loops. |
| 162 | let numberOfReRenders: number = 0; |
| 163 | const RE_RENDER_LIMIT = 25; |
| 164 | |
| 165 | let isInHookUserCodeInDev = false; |
| 166 | |
| 167 | // In DEV, this is the name of the currently executing primitive hook |
| 168 | let currentHookNameInDev: ?string; |
| 169 | |
| 170 | function resolveCurrentlyRenderingComponent(): Object { |
| 171 | if (currentlyRenderingComponent === null) { |
| 172 | throw new Error( |
| 173 | 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' + |
| 174 | ' one of the following reasons:\n' + |
| 175 | '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' + |
| 176 | '2. You might be breaking the Rules of Hooks\n' + |
| 177 | '3. You might have more than one copy of React in the same app\n' + |
| 178 | 'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.', |
| 179 | ); |
| 180 | } |
| 181 | |
| 182 | if (__DEV__) { |
| 183 | if (isInHookUserCodeInDev) { |
| 184 | console.error( |
| 185 | 'Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. ' + |
| 186 | 'You can only call Hooks at the top level of your React function. ' + |
| 187 | 'For more information, see ' + |
| 188 | 'https://react.dev/link/rules-of-hooks', |
| 189 | ); |
| 190 | } |
| 191 | } |
| 192 | return currentlyRenderingComponent; |
| 193 | } |
| 194 | |
| 195 | function areHookInputsEqual( |
| 196 | nextDeps: Array<mixed>, |
| 197 | prevDeps: Array<mixed> | null, |
| 198 | ) { |
| 199 | if (prevDeps === null) { |
| 200 | if (__DEV__) { |
| 201 | console.error( |
| 202 | '%s received a final argument during this render, but not during ' + |
| 203 | 'the previous render. Even though the final argument is optional, ' + |
| 204 | 'its type cannot change between renders.', |
| 205 | currentHookNameInDev, |
| 206 | ); |
| 207 | } |
| 208 | return false; |
| 209 | } |
| 210 | |
| 211 | if (__DEV__) { |
| 212 | // Don't bother comparing lengths in prod because these arrays should be |
| 213 | // passed inline. |
| 214 | if (nextDeps.length !== prevDeps.length) { |
| 215 | console.error( |
| 216 | 'The final argument passed to %s changed size between renders. The ' + |
| 217 | 'order and size of this array must remain constant.\n\n' + |
| 218 | 'Previous: %s\n' + |
| 219 | 'Incoming: %s', |
| 220 | currentHookNameInDev, |
| 221 | `[${nextDeps.join(', ')}]`, |
| 222 | `[${prevDeps.join(', ')}]`, |
| 223 | ); |
| 224 | } |
| 225 | } |
| 226 | // $FlowFixMe[incompatible-use] found when upgrading Flow |
| 227 | for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) { |
| 228 | // $FlowFixMe[incompatible-use] found when upgrading Flow |
| 229 | if (is(nextDeps[i], prevDeps[i])) { |
| 230 | continue; |
| 231 | } |
| 232 | return false; |
| 233 | } |
| 234 | return true; |
| 235 | } |
| 236 | |
| 237 | function createHook(): Hook { |
| 238 | if (numberOfReRenders > 0) { |
| 239 | throw new Error('Rendered more hooks than during the previous render'); |
| 240 | } |
| 241 | return { |
| 242 | memoizedState: null, |
| 243 | queue: null, |
| 244 | next: null, |
| 245 | }; |
| 246 | } |
| 247 | |
| 248 | function createWorkInProgressHook(): Hook { |
| 249 | if (workInProgressHook === null) { |
| 250 | // This is the first hook in the list |
| 251 | if (firstWorkInProgressHook === null) { |
| 252 | isReRender = false; |
| 253 | firstWorkInProgressHook = workInProgressHook = createHook(); |
| 254 | } else { |
| 255 | // There's already a work-in-progress. Reuse it. |
| 256 | isReRender = true; |
| 257 | workInProgressHook = firstWorkInProgressHook; |
| 258 | } |
| 259 | } else { |
| 260 | if (workInProgressHook.next === null) { |
| 261 | isReRender = false; |
| 262 | // Append to the end of the list |
| 263 | workInProgressHook = workInProgressHook.next = createHook(); |
| 264 | } else { |
| 265 | // There's already a work-in-progress. Reuse it. |
| 266 | isReRender = true; |
| 267 | workInProgressHook = workInProgressHook.next; |
| 268 | } |
| 269 | } |
| 270 | return workInProgressHook; |
| 271 | } |
| 272 | |
| 273 | export function prepareToUseHooks( |
| 274 | request: Request, |
| 275 | task: Task, |
| 276 | keyPath: KeyNode | null, |
| 277 | componentIdentity: Object, |
| 278 | prevThenableState: ThenableState | null, |
| 279 | ): void { |
| 280 | currentlyRenderingComponent = componentIdentity; |
| 281 | currentlyRenderingTask = task; |
| 282 | currentlyRenderingRequest = request; |
| 283 | currentlyRenderingKeyPath = keyPath; |
| 284 | if (__DEV__) { |
| 285 | isInHookUserCodeInDev = false; |
| 286 | } |
| 287 | |
| 288 | // The following should have already been reset |
| 289 | // didScheduleRenderPhaseUpdate = false; |
| 290 | // firstWorkInProgressHook = null; |
| 291 | // numberOfReRenders = 0; |
| 292 | // renderPhaseUpdates = null; |
| 293 | // workInProgressHook = null; |
| 294 | |
| 295 | localIdCounter = 0; |
| 296 | actionStateCounter = 0; |
| 297 | actionStateMatchingIndex = -1; |
| 298 | thenableIndexCounter = 0; |
| 299 | thenableState = prevThenableState; |
| 300 | } |
| 301 | |
| 302 | export function prepareToUseThenableState( |
| 303 | prevThenableState: ThenableState | null, |
| 304 | ): void { |
| 305 | thenableIndexCounter = 0; |
| 306 | thenableState = prevThenableState; |
| 307 | } |
| 308 | |
| 309 | export function finishHooks( |
| 310 | Component: any, |
| 311 | props: any, |
| 312 | children: any, |
| 313 | refOrContext: any, |
| 314 | ): any { |
| 315 | // This must be called after every function component to prevent hooks from |
| 316 | // being used in classes. |
| 317 | |
| 318 | while (didScheduleRenderPhaseUpdate) { |
| 319 | // Updates were scheduled during the render phase. They are stored in |
| 320 | // the `renderPhaseUpdates` map. Call the component again, reusing the |
| 321 | // work-in-progress hooks and applying the additional updates on top. Keep |
| 322 | // restarting until no more updates are scheduled. |
| 323 | didScheduleRenderPhaseUpdate = false; |
| 324 | localIdCounter = 0; |
| 325 | actionStateCounter = 0; |
| 326 | actionStateMatchingIndex = -1; |
| 327 | thenableIndexCounter = 0; |
| 328 | numberOfReRenders += 1; |
| 329 | |
| 330 | // Start over from the beginning of the list |
| 331 | workInProgressHook = null; |
| 332 | |
| 333 | children = Component(props, refOrContext); |
| 334 | } |
| 335 | |
| 336 | resetHooksState(); |
| 337 | return children; |
| 338 | } |
| 339 | |
| 340 | export function getThenableStateAfterSuspending(): null | ThenableState { |
| 341 | const state = thenableState; |
| 342 | thenableState = null; |
| 343 | return state; |
| 344 | } |
| 345 | |
| 346 | export function checkDidRenderIdHook(): boolean { |
| 347 | // This should be called immediately after every finishHooks call. |
| 348 | // Conceptually, it's part of the return value of finishHooks; it's only a |
| 349 | // separate function to avoid using an array tuple. |
| 350 | const didRenderIdHook = localIdCounter !== 0; |
| 351 | return didRenderIdHook; |
| 352 | } |
| 353 | |
| 354 | export function getActionStateCount(): number { |
| 355 | // This should be called immediately after every finishHooks call. |
| 356 | // Conceptually, it's part of the return value of finishHooks; it's only a |
| 357 | // separate function to avoid using an array tuple. |
| 358 | return actionStateCounter; |
| 359 | } |
| 360 | export function getActionStateMatchingIndex(): number { |
| 361 | // This should be called immediately after every finishHooks call. |
| 362 | // Conceptually, it's part of the return value of finishHooks; it's only a |
| 363 | // separate function to avoid using an array tuple. |
| 364 | return actionStateMatchingIndex; |
| 365 | } |
| 366 | |
| 367 | // Reset the internal hooks state if an error occurs while rendering a component |
| 368 | export function resetHooksState(): void { |
| 369 | if (__DEV__) { |
| 370 | isInHookUserCodeInDev = false; |
| 371 | } |
| 372 | |
| 373 | currentlyRenderingComponent = null; |
| 374 | currentlyRenderingTask = null; |
| 375 | currentlyRenderingRequest = null; |
| 376 | currentlyRenderingKeyPath = null; |
| 377 | didScheduleRenderPhaseUpdate = false; |
| 378 | firstWorkInProgressHook = null; |
| 379 | numberOfReRenders = 0; |
| 380 | renderPhaseUpdates = null; |
| 381 | workInProgressHook = null; |
| 382 | } |
| 383 | |
| 384 | function readContext<T>(context: ReactContext<T>): T { |
| 385 | if (__DEV__) { |
| 386 | if (isInHookUserCodeInDev) { |
| 387 | console.error( |
| 388 | 'Context can only be read while React is rendering. ' + |
| 389 | 'In classes, you can read it in the render method or getDerivedStateFromProps. ' + |
| 390 | 'In function components, you can read it directly in the function body, but not ' + |
| 391 | 'inside Hooks like useReducer() or useMemo().', |
| 392 | ); |
| 393 | } |
| 394 | } |
| 395 | return readContextImpl(context); |
| 396 | } |
| 397 | |
| 398 | function useContext<T>(context: ReactContext<T>): T { |
| 399 | if (__DEV__) { |
| 400 | currentHookNameInDev = 'useContext'; |
| 401 | } |
| 402 | resolveCurrentlyRenderingComponent(); |
| 403 | return readContextImpl(context); |
| 404 | } |
| 405 | |
| 406 | function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S { |
| 407 | // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types |
| 408 | return typeof action === 'function' ? action(state) : action; |
| 409 | } |
| 410 | |
| 411 | export function useState<S>( |
| 412 | initialState: (() => S) | S, |
| 413 | ): [S, Dispatch<BasicStateAction<S>>] { |
| 414 | if (__DEV__) { |
| 415 | currentHookNameInDev = 'useState'; |
| 416 | } |
| 417 | return useReducer( |
| 418 | basicStateReducer, |
| 419 | // useReducer has a special case to support lazy useState initializers |
| 420 | initialState as any, |
| 421 | ); |
| 422 | } |
| 423 | |
| 424 | export function useReducer<S, I, A>( |
| 425 | reducer: (S, A) => S, |
| 426 | initialArg: I, |
| 427 | init?: I => S, |
| 428 | ): [S, Dispatch<A>] { |
| 429 | if (__DEV__) { |
| 430 | // $FlowFixMe[invalid-compare] |
| 431 | if (reducer !== basicStateReducer) { |
| 432 | currentHookNameInDev = 'useReducer'; |
| 433 | } |
| 434 | } |
| 435 | currentlyRenderingComponent = resolveCurrentlyRenderingComponent(); |
| 436 | workInProgressHook = createWorkInProgressHook(); |
| 437 | if (isReRender) { |
| 438 | // This is a re-render. Apply the new render phase updates to the previous |
| 439 | // current hook. |
| 440 | const queue: UpdateQueue<A> = workInProgressHook.queue as any; |
| 441 | const dispatch: Dispatch<A> = queue.dispatch as any; |
| 442 | if (renderPhaseUpdates !== null) { |
| 443 | // Render phase updates are stored in a map of queue -> linked list |
| 444 | const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue); |
| 445 | if (firstRenderPhaseUpdate !== undefined) { |
| 446 | // $FlowFixMe[incompatible-use] found when upgrading Flow |
| 447 | renderPhaseUpdates.delete(queue); |
| 448 | // $FlowFixMe[incompatible-use] found when upgrading Flow |
| 449 | let newState = workInProgressHook.memoizedState; |
| 450 | let update: Update<any> = firstRenderPhaseUpdate; |
| 451 | do { |
| 452 | // Process this render phase update. We don't have to check the |
| 453 | // priority because it will always be the same as the current |
| 454 | // render's. |
| 455 | const action = update.action; |
| 456 | if (__DEV__) { |
| 457 | isInHookUserCodeInDev = true; |
| 458 | } |
| 459 | newState = reducer(newState, action); |
| 460 | if (__DEV__) { |
| 461 | isInHookUserCodeInDev = false; |
| 462 | } |
| 463 | // $FlowFixMe[incompatible-type] we bail out when we get a null |
| 464 | update = update.next; |
| 465 | } while (update !== null); |
| 466 | |
| 467 | // $FlowFixMe[incompatible-use] found when upgrading Flow |
| 468 | workInProgressHook.memoizedState = newState; |
| 469 | |
| 470 | return [newState, dispatch]; |
| 471 | } |
| 472 | } |
| 473 | // $FlowFixMe[incompatible-use] found when upgrading Flow |
| 474 | return [workInProgressHook.memoizedState, dispatch]; |
| 475 | } else { |
| 476 | if (__DEV__) { |
| 477 | isInHookUserCodeInDev = true; |
| 478 | } |
| 479 | let initialState; |
| 480 | // $FlowFixMe[invalid-compare] |
| 481 | if (reducer === basicStateReducer) { |
| 482 | // Special case for `useState`. |
| 483 | initialState = |
| 484 | typeof initialArg === 'function' |
| 485 | ? (initialArg as any as () => S)() |
| 486 | : (initialArg as any as S); |
| 487 | } else { |
| 488 | initialState = |
| 489 | init !== undefined ? init(initialArg) : (initialArg as any as S); |
| 490 | } |
| 491 | if (__DEV__) { |
| 492 | isInHookUserCodeInDev = false; |
| 493 | } |
| 494 | // $FlowFixMe[incompatible-use] found when upgrading Flow |
| 495 | workInProgressHook.memoizedState = initialState; |
| 496 | // $FlowFixMe[incompatible-use] found when upgrading Flow |
| 497 | const queue: UpdateQueue<A> = (workInProgressHook.queue = { |
| 498 | last: null, |
| 499 | dispatch: null, |
| 500 | }); |
| 501 | const dispatch: Dispatch<A> = (queue.dispatch = dispatchAction.bind( |
| 502 | null, |
| 503 | currentlyRenderingComponent, |
| 504 | queue, |
| 505 | ) as any); |
| 506 | // $FlowFixMe[incompatible-use] found when upgrading Flow |
| 507 | return [workInProgressHook.memoizedState, dispatch]; |
| 508 | } |
| 509 | } |
| 510 | |
| 511 | function useMemo<T>(nextCreate: () => T, deps: Array<mixed> | void | null): T { |
| 512 | currentlyRenderingComponent = resolveCurrentlyRenderingComponent(); |
| 513 | workInProgressHook = createWorkInProgressHook(); |
| 514 | |
| 515 | const nextDeps = deps === undefined ? null : deps; |
| 516 | |
| 517 | // $FlowFixMe[invalid-compare] |
| 518 | if (workInProgressHook !== null) { |
| 519 | const prevState = workInProgressHook.memoizedState; |
| 520 | if (prevState !== null) { |
| 521 | if (nextDeps !== null) { |
| 522 | const prevDeps = prevState[1]; |
| 523 | if (areHookInputsEqual(nextDeps, prevDeps)) { |
| 524 | return prevState[0]; |
| 525 | } |
| 526 | } |
| 527 | } |
| 528 | } |
| 529 | |
| 530 | if (__DEV__) { |
| 531 | isInHookUserCodeInDev = true; |
| 532 | } |
| 533 | const nextValue = nextCreate(); |
| 534 | if (__DEV__) { |
| 535 | isInHookUserCodeInDev = false; |
| 536 | } |
| 537 | // $FlowFixMe[incompatible-use] found when upgrading Flow |
| 538 | workInProgressHook.memoizedState = [nextValue, nextDeps]; |
| 539 | return nextValue; |
| 540 | } |
| 541 | |
| 542 | function useRef<T>(initialValue: T): {current: T} { |
| 543 | currentlyRenderingComponent = resolveCurrentlyRenderingComponent(); |
| 544 | workInProgressHook = createWorkInProgressHook(); |
| 545 | const previousRef = workInProgressHook.memoizedState; |
| 546 | if (previousRef === null) { |
| 547 | const ref = {current: initialValue}; |
| 548 | if (__DEV__) { |
| 549 | Object.seal(ref); |
| 550 | } |
| 551 | // $FlowFixMe[incompatible-use] found when upgrading Flow |
| 552 | workInProgressHook.memoizedState = ref; |
| 553 | return ref; |
| 554 | } else { |
| 555 | return previousRef; |
| 556 | } |
| 557 | } |
| 558 | |
| 559 | function dispatchAction<A>( |
| 560 | componentIdentity: Object, |
| 561 | queue: UpdateQueue<A>, |
| 562 | action: A, |
| 563 | ): void { |
| 564 | if (numberOfReRenders >= RE_RENDER_LIMIT) { |
| 565 | throw new Error( |
| 566 | 'Too many re-renders. React limits the number of renders to prevent ' + |
| 567 | 'an infinite loop.', |
| 568 | ); |
| 569 | } |
| 570 | |
| 571 | if (componentIdentity === currentlyRenderingComponent) { |
| 572 | // This is a render phase update. Stash it in a lazily-created map of |
| 573 | // queue -> linked list of updates. After this render pass, we'll restart |
| 574 | // and apply the stashed updates on top of the work-in-progress hook. |
| 575 | didScheduleRenderPhaseUpdate = true; |
| 576 | const update: Update<A> = { |
| 577 | action, |
| 578 | next: null, |
| 579 | }; |
| 580 | if (renderPhaseUpdates === null) { |
| 581 | renderPhaseUpdates = new Map(); |
| 582 | } |
| 583 | const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue); |
| 584 | if (firstRenderPhaseUpdate === undefined) { |
| 585 | // $FlowFixMe[incompatible-use] found when upgrading Flow |
| 586 | renderPhaseUpdates.set(queue, update); |
| 587 | } else { |
| 588 | // Append the update to the end of the list. |
| 589 | let lastRenderPhaseUpdate = firstRenderPhaseUpdate; |
| 590 | while (lastRenderPhaseUpdate.next !== null) { |
| 591 | lastRenderPhaseUpdate = lastRenderPhaseUpdate.next; |
| 592 | } |
| 593 | lastRenderPhaseUpdate.next = update; |
| 594 | } |
| 595 | } else { |
| 596 | // This means an update has happened after the function component has |
| 597 | // returned. On the server this is a no-op. In React Fiber, the update |
| 598 | // would be scheduled for a future render. |
| 599 | } |
| 600 | } |
| 601 | |
| 602 | export function useCallback<T>( |
| 603 | callback: T, |
| 604 | deps: Array<mixed> | void | null, |
| 605 | ): T { |
| 606 | return useMemo(() => callback, deps); |
| 607 | } |
| 608 | |
| 609 | function throwOnUseEffectEventCall() { |
| 610 | throw new Error( |
| 611 | "A function wrapped in useEffectEvent can't be called during rendering.", |
| 612 | ); |
| 613 | } |
| 614 | |
| 615 | export function useEffectEvent<Args, Return, F: (...Array<Args>) => Return>( |
| 616 | callback: F, |
| 617 | ): F { |
| 618 | // $FlowFixMe[incompatible-type] |
| 619 | return throwOnUseEffectEventCall; |
| 620 | } |
| 621 | |
| 622 | function useSyncExternalStore<T>( |
| 623 | subscribe: (() => void) => () => void, |
| 624 | getSnapshot: () => T, |
| 625 | getServerSnapshot?: () => T, |
| 626 | ): T { |
| 627 | if (getServerSnapshot === undefined) { |
| 628 | throw new Error( |
| 629 | 'Missing getServerSnapshot, which is required for ' + |
| 630 | 'server-rendered content. Will revert to client rendering.', |
| 631 | ); |
| 632 | } |
| 633 | return getServerSnapshot(); |
| 634 | } |
| 635 | |
| 636 | function useDeferredValue<T>(value: T, initialValue?: T): T { |
| 637 | resolveCurrentlyRenderingComponent(); |
| 638 | return initialValue !== undefined ? initialValue : value; |
| 639 | } |
| 640 | |
| 641 | function unsupportedStartTransition() { |
| 642 | throw new Error('startTransition cannot be called during server rendering.'); |
| 643 | } |
| 644 | |
| 645 | function useTransition(): [ |
| 646 | boolean, |
| 647 | (callback: () => void, options?: StartTransitionOptions) => void, |
| 648 | ] { |
| 649 | resolveCurrentlyRenderingComponent(); |
| 650 | return [false, unsupportedStartTransition]; |
| 651 | } |
| 652 | |
| 653 | function useHostTransitionStatus(): TransitionStatus { |
| 654 | resolveCurrentlyRenderingComponent(); |
| 655 | return NotPendingTransition; |
| 656 | } |
| 657 | |
| 658 | function unsupportedSetOptimisticState() { |
| 659 | throw new Error('Cannot update optimistic state while rendering.'); |
| 660 | } |
| 661 | |
| 662 | function useOptimistic<S, A>( |
| 663 | passthrough: S, |
| 664 | reducer: ?(S, A) => S, |
| 665 | ): [S, (A) => void] { |
| 666 | resolveCurrentlyRenderingComponent(); |
| 667 | return [passthrough, unsupportedSetOptimisticState]; |
| 668 | } |
| 669 | |
| 670 | function createPostbackActionStateKey( |
| 671 | permalink: string | void, |
| 672 | componentKeyPath: KeyNode | null, |
| 673 | hookIndex: number, |
| 674 | ): string { |
| 675 | if (permalink !== undefined) { |
| 676 | // Don't bother to hash a permalink-based key since it's already short. |
| 677 | return 'p' + permalink; |
| 678 | } else { |
| 679 | // Append a node to the key path that represents the form state hook. |
| 680 | const keyPath: KeyNode = [componentKeyPath, null, hookIndex]; |
| 681 | // Key paths are hashed to reduce the size. It does not need to be secure, |
| 682 | // and it's more important that it's fast than that it's completely |
| 683 | // collision-free. |
| 684 | const keyPathHash = createFastHash(JSON.stringify(keyPath)); |
| 685 | return 'k' + keyPathHash; |
| 686 | } |
| 687 | } |
| 688 | |
| 689 | function useActionState<S, P>( |
| 690 | action: (Awaited<S>, P) => S, |
| 691 | initialState: Awaited<S>, |
| 692 | permalink?: string, |
| 693 | ): [Awaited<S>, (P) => void, boolean] { |
| 694 | resolveCurrentlyRenderingComponent(); |
| 695 | |
| 696 | // Count the number of useActionState hooks per component. We also use this to |
| 697 | // track the position of this useActionState hook relative to the other ones in |
| 698 | // this component, so we can generate a unique key for each one. |
| 699 | const actionStateHookIndex = actionStateCounter++; |
| 700 | const request: Request = currentlyRenderingRequest as any; |
| 701 | |
| 702 | // $FlowFixMe[prop-missing] |
| 703 | const formAction = action.$$FORM_ACTION; |
| 704 | if (typeof formAction === 'function') { |
| 705 | // This is a server action. These have additional features to enable |
| 706 | // MPA-style form submissions with progressive enhancement. |
| 707 | |
| 708 | // TODO: If the same permalink is passed to multiple useActionStates, and |
| 709 | // they all have the same action signature, Fizz will pass the postback |
| 710 | // state to all of them. We should probably only pass it to the first one, |
| 711 | // and/or warn. |
| 712 | |
| 713 | // The key is lazily generated and deduped so the that the keypath doesn't |
| 714 | // get JSON.stringify-ed unnecessarily, and at most once. |
| 715 | let nextPostbackStateKey = null; |
| 716 | |
| 717 | // Determine the current form state. If we received state during an MPA form |
| 718 | // submission, then we will reuse that, if the action identity matches. |
| 719 | // Otherwise, we'll use the initial state argument. We will emit a comment |
| 720 | // marker into the stream that indicates whether the state was reused. |
| 721 | let state = initialState; |
| 722 | const componentKeyPath = currentlyRenderingKeyPath as any; |
| 723 | const postbackActionState = getFormState(request); |
| 724 | // $FlowFixMe[prop-missing] |
| 725 | const isSignatureEqual = action.$$IS_SIGNATURE_EQUAL; |
| 726 | if ( |
| 727 | postbackActionState !== null && |
| 728 | typeof isSignatureEqual === 'function' |
| 729 | ) { |
| 730 | const postbackKey = postbackActionState[1]; |
| 731 | const postbackReferenceId = postbackActionState[2]; |
| 732 | const postbackBoundArity = postbackActionState[3]; |
| 733 | if ( |
| 734 | isSignatureEqual.call(action, postbackReferenceId, postbackBoundArity) |
| 735 | ) { |
| 736 | nextPostbackStateKey = createPostbackActionStateKey( |
| 737 | permalink, |
| 738 | componentKeyPath, |
| 739 | actionStateHookIndex, |
| 740 | ); |
| 741 | if (postbackKey === nextPostbackStateKey) { |
| 742 | // This was a match |
| 743 | actionStateMatchingIndex = actionStateHookIndex; |
| 744 | // Reuse the state that was submitted by the form. |
| 745 | state = postbackActionState[0]; |
| 746 | } |
| 747 | } |
| 748 | } |
| 749 | |
| 750 | // Bind the state to the first argument of the action. |
| 751 | const boundAction = action.bind(null, state); |
| 752 | |
| 753 | // Wrap the action so the return value is void. |
| 754 | const dispatch = (payload: P): void => { |
| 755 | boundAction(payload); |
| 756 | }; |
| 757 | |
| 758 | // $FlowIgnore[prop-missing] |
| 759 | if (typeof boundAction.$$FORM_ACTION === 'function') { |
| 760 | // $FlowFixMe[prop-missing] |
| 761 | dispatch.$$FORM_ACTION = (prefix: string) => { |
| 762 | const metadata: ReactCustomFormAction = |
| 763 | boundAction.$$FORM_ACTION(prefix); |
| 764 | |
| 765 | // Override the action URL |
| 766 | if (permalink !== undefined) { |
| 767 | if (__DEV__) { |
| 768 | checkAttributeStringCoercion(permalink, 'target'); |
| 769 | } |
| 770 | permalink += ''; |
| 771 | metadata.action = permalink; |
| 772 | } |
| 773 | |
| 774 | const formData = metadata.data; |
| 775 | if (formData) { |
| 776 | if (nextPostbackStateKey === null) { |
| 777 | nextPostbackStateKey = createPostbackActionStateKey( |
| 778 | permalink, |
| 779 | componentKeyPath, |
| 780 | actionStateHookIndex, |
| 781 | ); |
| 782 | } |
| 783 | formData.append('$ACTION_KEY', nextPostbackStateKey); |
| 784 | } |
| 785 | return metadata; |
| 786 | }; |
| 787 | } |
| 788 | |
| 789 | return [state, dispatch, false]; |
| 790 | } else { |
| 791 | // This is not a server action, so the implementation is much simpler. |
| 792 | |
| 793 | // Bind the state to the first argument of the action. |
| 794 | const boundAction = action.bind(null, initialState); |
| 795 | // Wrap the action so the return value is void. |
| 796 | const dispatch = (payload: P): void => { |
| 797 | boundAction(payload); |
| 798 | }; |
| 799 | return [initialState, dispatch, false]; |
| 800 | } |
| 801 | } |
| 802 | |
| 803 | function useId(): string { |
| 804 | const task: Task = currentlyRenderingTask as any; |
| 805 | const treeId = getTreeId(task.treeContext); |
| 806 | |
| 807 | const resumableState = currentResumableState; |
| 808 | if (resumableState === null) { |
| 809 | throw new Error( |
| 810 | 'Invalid hook call. Hooks can only be called inside of the body of a function component.', |
| 811 | ); |
| 812 | } |
| 813 | |
| 814 | const localId = localIdCounter++; |
| 815 | return makeId(resumableState, treeId, localId); |
| 816 | } |
| 817 | |
| 818 | function use<T>(usable: Usable<T>): T { |
| 819 | // $FlowFixMe[invalid-compare] |
| 820 | if (usable !== null && typeof usable === 'object') { |
| 821 | // $FlowFixMe[method-unbinding] |
| 822 | if (typeof usable.then === 'function') { |
| 823 | // This is a thenable. |
| 824 | const thenable: Thenable<T> = usable as any; |
| 825 | return unwrapThenable(thenable); |
| 826 | } else if (usable.$$typeof === REACT_RECOVERABLE_TYPE) { |
| 827 | // Create the recoverable error here so its stack captures the component |
| 828 | // that passed this value to use(). The internal brand lets the renderer |
| 829 | // distinguish it from an Error thrown by application code. |
| 830 | const recoverable: ReactRecoverable = usable as any; |
| 831 | throw createRecoverableError(recoverable); |
| 832 | } else if (usable.$$typeof === REACT_CONTEXT_TYPE) { |
| 833 | const context: ReactContext<T> = usable as any; |
| 834 | return readContext(context); |
| 835 | } |
| 836 | } |
| 837 | |
| 838 | // eslint-disable-next-line react-internal/safe-string-coercion |
| 839 | throw new Error('An unsupported type was passed to use(): ' + String(usable)); |
| 840 | } |
| 841 | |
| 842 | export function unwrapThenable<T>(thenable: Thenable<T>): T { |
| 843 | const index = thenableIndexCounter; |
| 844 | thenableIndexCounter += 1; |
| 845 | if (thenableState === null) { |
| 846 | thenableState = createThenableState(); |
| 847 | } |
| 848 | return trackUsedThenable(thenableState, thenable, index); |
| 849 | } |
| 850 | |
| 851 | export function readPreviousThenableFromState<T>(): T | void { |
| 852 | const index = thenableIndexCounter; |
| 853 | thenableIndexCounter += 1; |
| 854 | if (thenableState === null) { |
| 855 | return undefined; |
| 856 | } |
| 857 | return readPreviousThenable(thenableState, index); |
| 858 | } |
| 859 | |
| 860 | function unsupportedRefresh() { |
| 861 | throw new Error('Cache cannot be refreshed during server rendering.'); |
| 862 | } |
| 863 | |
| 864 | function useCacheRefresh(): <T>(?() => T, ?T) => void { |
| 865 | return unsupportedRefresh; |
| 866 | } |
| 867 | |
| 868 | function useMemoCache(size: number): Array<mixed> { |
| 869 | const data = new Array<any>(size); |
| 870 | for (let i = 0; i < size; i++) { |
| 871 | data[i] = REACT_MEMO_CACHE_SENTINEL; |
| 872 | } |
| 873 | return data; |
| 874 | } |
| 875 | |
| 876 | function clientHookNotSupported() { |
| 877 | throw new Error( |
| 878 | 'Cannot use state or effect Hooks in renderToHTML because ' + |
| 879 | 'this component will never be hydrated.', |
| 880 | ); |
| 881 | } |
| 882 | |
| 883 | // $FlowFixMe[constant-condition] |
| 884 | export const HooksDispatcher: Dispatcher = supportsClientAPIs |
| 885 | ? { |
| 886 | readContext, |
| 887 | use, |
| 888 | useContext, |
| 889 | useMemo, |
| 890 | useReducer, |
| 891 | useRef, |
| 892 | useState, |
| 893 | useInsertionEffect: noop, |
| 894 | useLayoutEffect: noop, |
| 895 | useCallback, |
| 896 | // useImperativeHandle is not run in the server environment |
| 897 | useImperativeHandle: noop, |
| 898 | // Effects are not run in the server environment. |
| 899 | useEffect: noop, |
| 900 | // Debugging effect |
| 901 | useDebugValue: noop, |
| 902 | useDeferredValue, |
| 903 | useTransition, |
| 904 | useId, |
| 905 | // Subscriptions are not setup in a server environment. |
| 906 | useSyncExternalStore, |
| 907 | useOptimistic, |
| 908 | useActionState, |
| 909 | useFormState: useActionState, |
| 910 | useHostTransitionStatus, |
| 911 | useMemoCache, |
| 912 | useCacheRefresh, |
| 913 | useEffectEvent, |
| 914 | } |
| 915 | : { |
| 916 | readContext, |
| 917 | use, |
| 918 | useCallback, |
| 919 | useContext, |
| 920 | useEffect: clientHookNotSupported, |
| 921 | useImperativeHandle: clientHookNotSupported, |
| 922 | useInsertionEffect: clientHookNotSupported, |
| 923 | useLayoutEffect: clientHookNotSupported, |
| 924 | useMemo, |
| 925 | useReducer: clientHookNotSupported, |
| 926 | useRef: clientHookNotSupported, |
| 927 | useState: clientHookNotSupported, |
| 928 | useDebugValue: noop, |
| 929 | useDeferredValue: clientHookNotSupported, |
| 930 | useTransition: clientHookNotSupported, |
| 931 | useSyncExternalStore: clientHookNotSupported, |
| 932 | useId, |
| 933 | useHostTransitionStatus, |
| 934 | useFormState: useActionState, |
| 935 | useActionState, |
| 936 | useOptimistic, |
| 937 | useMemoCache, |
| 938 | useCacheRefresh, |
| 939 | useEffectEvent, |
| 940 | }; |
| 941 | |
| 942 | export let currentResumableState: null | ResumableState = null as any; |
| 943 | export function setCurrentResumableState( |
| 944 | resumableState: null | ResumableState, |
| 945 | ): void { |
| 946 | currentResumableState = resumableState; |
| 947 | } |