main
js 947 lines 29.7 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 {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 }