1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 *7 * @flow8 */910import type {11 ReactContext,12 StartTransitionOptions,13 Usable,14 Thenable,15 RejectedThenable,16 Awaited,17} from 'shared/ReactTypes';18import type {19 Fiber,20 FiberRoot,21 Dispatcher,22 HookType,23 MemoCache,24} from './ReactInternalTypes';25import type {Lanes, Lane} from './ReactFiberLane';26import type {HookFlags} from './ReactHookEffectTags';27import type {Flags} from './ReactFiberFlags';28import type {TransitionStatus} from './ReactFiberConfig';29import type {ScheduledGesture} from './ReactFiberGestureScheduler';3031import {32 HostTransitionContext,33 NotPendingTransition as NoPendingHostTransition,34 setCurrentUpdatePriority,35 getCurrentUpdatePriority,36} from './ReactFiberConfig';37import ReactSharedInternals from 'shared/ReactSharedInternals';38import {39 enableSchedulingProfiler,40 enableTransitionTracing,41 enableLegacyCache,42 disableLegacyMode,43 enableNoCloningMemoCache,44 enableViewTransition,45 enableGestureTransition,46} from 'shared/ReactFeatureFlags';47import {48 REACT_CONTEXT_TYPE,49 REACT_MEMO_CACHE_SENTINEL,50} from 'shared/ReactSymbols';5152import {53 NoMode,54 ConcurrentMode,55 StrictEffectsMode,56 StrictLegacyMode,57} from './ReactTypeOfMode';58import {59 NoLane,60 SyncLane,61 OffscreenLane,62 DeferredLane,63 NoLanes,64 isSubsetOfLanes,65 includesBlockingLane,66 includesOnlyNonUrgentLanes,67 mergeLanes,68 removeLanes,69 intersectLanes,70 isTransitionLane,71 markRootEntangled,72 includesSomeLane,73 isGestureRender,74 GestureLane,75 UpdateLanes,76} from './ReactFiberLane';77import {78 ContinuousEventPriority,79 higherEventPriority,80} from './ReactEventPriorities';81import {readContext, checkIfContextChanged} from './ReactFiberNewContext';82import {HostRoot, CacheComponent, HostComponent} from './ReactWorkTags';83import {84 LayoutStatic as LayoutStaticEffect,85 Passive as PassiveEffect,86 PassiveStatic as PassiveStaticEffect,87 StaticMask as StaticMaskEffect,88 Update as UpdateEffect,89 StoreConsistency,90 MountLayoutDev as MountLayoutDevEffect,91 MountPassiveDev as MountPassiveDevEffect,92 FormReset,93} from './ReactFiberFlags';94import {95 HasEffect as HookHasEffect,96 Layout as HookLayout,97 Passive as HookPassive,98 Insertion as HookInsertion,99} from './ReactHookEffectTags';100import {101 getWorkInProgressRoot,102 getWorkInProgressRootRenderLanes,103 scheduleUpdateOnFiber,104 requestUpdateLane,105 requestDeferredLane,106 markSkippedUpdateLanes,107 isInvalidExecutionContextForEventFunction,108} from './ReactFiberWorkLoop';109110import getComponentNameFromFiber from 'react-reconciler/src/getComponentNameFromFiber';111import is from 'shared/objectIs';112import isArray from 'shared/isArray';113import {114 markWorkInProgressReceivedUpdate,115 checkIfWorkInProgressReceivedUpdate,116} from './ReactFiberBeginWork';117import {118 getIsHydrating,119 tryToClaimNextHydratableFormMarkerInstance,120} from './ReactFiberHydrationContext';121import {122 markStateUpdateScheduled,123 setIsStrictModeForDevtools,124} from './ReactFiberDevToolsHook';125import {126 startUpdateTimerByLane,127 startHostActionTimer,128} from './ReactProfilerTimer';129import {createCache} from './ReactFiberCacheComponent';130import {131 createUpdate as createLegacyQueueUpdate,132 enqueueUpdate as enqueueLegacyQueueUpdate,133 entangleTransitions as entangleLegacyQueueTransitions,134} from './ReactFiberClassUpdateQueue';135import {136 enqueueConcurrentHookUpdate,137 enqueueConcurrentHookUpdateAndEagerlyBailout,138 enqueueConcurrentRenderForLane,139} from './ReactFiberConcurrentUpdates';140import {getTreeId} from './ReactFiberTreeContext';141import {now} from './Scheduler';142import {143 trackUsedThenable,144 checkIfUseWrappedInTryCatch,145 createThenableState,146 SuspenseException,147 SuspenseActionException,148} from './ReactFiberThenable';149import type {ThenableState} from './ReactFiberThenable';150import type {Transition} from 'react/src/ReactStartTransition';151import {152 peekEntangledActionLane,153 peekEntangledActionThenable,154 chainThenableValue,155} from './ReactFiberAsyncAction';156import {requestTransitionLane} from './ReactFiberRootScheduler';157import {isCurrentTreeHidden} from './ReactFiberHiddenContext';158import {requestCurrentTransition} from './ReactFiberTransition';159160import {callComponentInDEV} from './ReactFiberCallUserSpace';161162import {scheduleGesture} from './ReactFiberGestureScheduler';163164export type Update<S, A> = {165 lane: Lane,166 revertLane: Lane,167 action: A,168 hasEagerState: boolean,169 eagerState: S | null,170 next: Update<S, A>,171 gesture: null | ScheduledGesture, // enableGestureTransition172};173174export type UpdateQueue<S, A> = {175 pending: Update<S, A> | null,176 lanes: Lanes,177 dispatch: (A => mixed) | null,178 lastRenderedReducer: ((S, A) => S) | null,179 lastRenderedState: S | null,180};181182let didWarnAboutMismatchedHooksForComponent;183let didWarnUncachedGetSnapshot: void | true;184let didWarnAboutUseWrappedInTryCatch;185let didWarnAboutAsyncClientComponent;186let didWarnAboutUseFormState;187if (__DEV__) {188 didWarnAboutMismatchedHooksForComponent = new Set<string | null>();189 didWarnAboutUseWrappedInTryCatch = new Set<string | null>();190 didWarnAboutAsyncClientComponent = new Set<string | null>();191 didWarnAboutUseFormState = new Set<string | null>();192}193194export type Hook = {195 memoizedState: any,196 baseState: any,197 baseQueue: Update<any, any> | null,198 queue: any,199 next: Hook | null,200};201202// The effect "instance" is a shared object that remains the same for the entire203// lifetime of an effect. In Rust terms, a RefCell. We use it to store the204// "destroy" function that is returned from an effect, because that is stateful.205// The field is `undefined` if the effect is unmounted, or if the effect ran206// but is not stateful. We don't explicitly track whether the effect is mounted207// or unmounted because that can be inferred by the hiddenness of the fiber in208// the tree, i.e. whether there is a hidden Offscreen fiber above it.209//210// It's unfortunate that this is stored on a separate object, because it adds211// more memory per effect instance, but it's conceptually sound. I think there's212// likely a better data structure we could use for effects; perhaps just one213// array of effect instances per fiber. But I think this is OK for now despite214// the additional memory and we can follow up with performance215// optimizations later.216type EffectInstance = {217 destroy: void | (() => void),218};219220export type Effect = {221 tag: HookFlags,222 inst: EffectInstance,223 create: () => (() => void) | void,224 deps: Array<mixed> | void | null,225 next: Effect,226};227228type StoreInstance<T> = {229 value: T,230 getSnapshot: () => T,231};232233type StoreConsistencyCheck<T> = {234 value: T,235 getSnapshot: () => T,236};237238type EventFunctionPayload<Args, Return, F: (...Array<Args>) => Return> = {239 ref: {240 eventFn: F,241 impl: F,242 },243 nextImpl: F,244};245246export type FunctionComponentUpdateQueue = {247 lastEffect: Effect | null,248 events: Array<EventFunctionPayload<any, any, any>> | null,249 stores: Array<StoreConsistencyCheck<any>> | null,250 memoCache: MemoCache | null,251};252253type BasicStateAction<S> = (S => S) | S;254255type Dispatch<A> = A => void;256257// These are set right before calling the component.258let renderLanes: Lanes = NoLanes;259// The work-in-progress fiber. I've named it differently to distinguish it from260// the work-in-progress hook.261let currentlyRenderingFiber: Fiber = null as any;262263// Hooks are stored as a linked list on the fiber's memoizedState field. The264// current hook list is the list that belongs to the current fiber. The265// work-in-progress hook list is a new list that will be added to the266// work-in-progress fiber.267let currentHook: Hook | null = null;268let workInProgressHook: Hook | null = null;269270// Whether an update was scheduled at any point during the render phase. This271// does not get reset if we do another render pass; only when we're completely272// finished evaluating this component. This is an optimization so we know273// whether we need to clear render phase updates after a throw.274let didScheduleRenderPhaseUpdate: boolean = false;275// Where an update was scheduled only during the current render pass. This276// gets reset after each attempt.277// TODO: Maybe there's some way to consolidate this with278// `didScheduleRenderPhaseUpdate`. Or with `numberOfReRenders`.279let didScheduleRenderPhaseUpdateDuringThisPass: boolean = false;280let shouldDoubleInvokeUserFnsInHooksDEV: boolean = false;281// Counts the number of useId hooks in this component.282let localIdCounter: number = 0;283// Counts number of `use`-d thenables284let thenableIndexCounter: number = 0;285let thenableState: ThenableState | null = null;286287// Used for ids that are generated completely client-side (i.e. not during288// hydration). This counter is global, so client ids are not stable across289// render attempts.290let globalClientIdCounter: number = 0;291292const RE_RENDER_LIMIT = 25;293294// In DEV, this is the name of the currently executing primitive hook295let currentHookNameInDev: ?HookType = null;296297// In DEV, this list ensures that hooks are called in the same order between renders.298// The list stores the order of hooks used during the initial render (mount).299// Subsequent renders (updates) reference this list.300let hookTypesDev: Array<HookType> | null = null;301let hookTypesUpdateIndexDev: number = -1;302303// In DEV, this tracks whether currently rendering component needs to ignore304// the dependencies for Hooks that need them (e.g. useEffect or useMemo).305// When true, such Hooks will always be "remounted". Only used during hot reload.306let ignorePreviousDependencies: boolean = false;307308function mountHookTypesDev(): void {309 if (__DEV__) {310 const hookName = currentHookNameInDev as any as HookType;311312 if (hookTypesDev === null) {313 hookTypesDev = [hookName];314 } else {315 hookTypesDev.push(hookName);316 }317 }318}319320function updateHookTypesDev(): void {321 if (__DEV__) {322 const hookName = currentHookNameInDev as any as HookType;323324 if (hookTypesDev !== null) {325 hookTypesUpdateIndexDev++;326 if (hookTypesDev[hookTypesUpdateIndexDev] !== hookName) {327 warnOnHookMismatchInDev(hookName);328 }329 }330 }331}332333function checkDepsAreArrayDev(deps: mixed): void {334 if (__DEV__) {335 if (deps !== undefined && deps !== null && !isArray(deps)) {336 // Verify deps, but only on mount to avoid extra checks.337 // It's unlikely their type would change as usually you define them inline.338 console.error(339 '%s received a final argument that is not an array (instead, received `%s`). When ' +340 'specified, the final argument must be an array.',341 currentHookNameInDev,342 typeof deps,343 );344 }345 }346}347348function warnOnHookMismatchInDev(currentHookName: HookType): void {349 if (__DEV__) {350 const componentName = getComponentNameFromFiber(currentlyRenderingFiber);351 if (!didWarnAboutMismatchedHooksForComponent.has(componentName)) {352 didWarnAboutMismatchedHooksForComponent.add(componentName);353354 if (hookTypesDev !== null) {355 let table = '';356357 const secondColumnStart = 30;358359 for (let i = 0; i <= (hookTypesUpdateIndexDev as any as number); i++) {360 const oldHookName = hookTypesDev[i];361 const newHookName =362 i === (hookTypesUpdateIndexDev as any as number)363 ? currentHookName364 : oldHookName;365366 let row = `${i + 1}. ${oldHookName}`;367368 // Extra space so second column lines up369 // lol @ IE not supporting String#repeat370 while (row.length < secondColumnStart) {371 row += ' ';372 }373374 row += newHookName + '\n';375376 table += row;377 }378379 console.error(380 'React has detected a change in the order of Hooks called by %s. ' +381 'This will lead to bugs and errors if not fixed. ' +382 'For more information, read the Rules of Hooks: https://react.dev/link/rules-of-hooks\n\n' +383 ' Previous render Next render\n' +384 ' ------------------------------------------------------\n' +385 '%s' +386 ' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n',387 componentName,388 table,389 );390 }391 }392 }393}394395function warnOnUseFormStateInDev(): void {396 if (__DEV__) {397 const componentName = getComponentNameFromFiber(currentlyRenderingFiber);398 if (!didWarnAboutUseFormState.has(componentName)) {399 didWarnAboutUseFormState.add(componentName);400401 console.error(402 'ReactDOM.useFormState has been renamed to React.useActionState. ' +403 'Please update %s to use React.useActionState.',404 componentName,405 );406 }407 }408}409410function warnIfAsyncClientComponent(Component: Function) {411 if (__DEV__) {412 // This dev-only check only works for detecting native async functions,413 // not transpiled ones. There's also a prod check that we use to prevent414 // async client components from crashing the app; the prod one works even415 // for transpiled async functions. Neither mechanism is completely416 // bulletproof but together they cover the most common cases.417 const isAsyncFunction =418 // $FlowFixMe[method-unbinding]419 Object.prototype.toString.call(Component) === '[object AsyncFunction]' ||420 // $FlowFixMe[method-unbinding]421 Object.prototype.toString.call(Component) ===422 '[object AsyncGeneratorFunction]';423 if (isAsyncFunction) {424 // Encountered an async Client Component. This is not yet supported.425 const componentName = getComponentNameFromFiber(currentlyRenderingFiber);426 if (!didWarnAboutAsyncClientComponent.has(componentName)) {427 didWarnAboutAsyncClientComponent.add(componentName);428 console.error(429 '%s is an async Client Component. ' +430 'Only Server Components can be async at the moment. This error is often caused by accidentally ' +431 "adding `'use client'` to a module that was originally written " +432 'for the server.',433 componentName === null434 ? 'An unknown Component'435 : `<${componentName}>`,436 );437 }438 }439 }440}441442function throwInvalidHookError() {443 throw new Error(444 'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +445 ' one of the following reasons:\n' +446 '1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +447 '2. You might be breaking the Rules of Hooks\n' +448 '3. You might have more than one copy of React in the same app\n' +449 'See https://react.dev/link/invalid-hook-call for tips about how to debug and fix this problem.',450 );451}452453function areHookInputsEqual(454 nextDeps: Array<mixed>,455 prevDeps: Array<mixed> | null,456): boolean {457 if (__DEV__) {458 if (ignorePreviousDependencies) {459 // Only true when this component is being hot reloaded.460 return false;461 }462 }463464 if (prevDeps === null) {465 if (__DEV__) {466 console.error(467 '%s received a final argument during this render, but not during ' +468 'the previous render. Even though the final argument is optional, ' +469 'its type cannot change between renders.',470 currentHookNameInDev,471 );472 }473 return false;474 }475476 if (__DEV__) {477 // Don't bother comparing lengths in prod because these arrays should be478 // passed inline.479 if (nextDeps.length !== prevDeps.length) {480 console.error(481 'The final argument passed to %s changed size between renders. The ' +482 'order and size of this array must remain constant.\n\n' +483 'Previous: %s\n' +484 'Incoming: %s',485 currentHookNameInDev,486 `[${prevDeps.join(', ')}]`,487 `[${nextDeps.join(', ')}]`,488 );489 }490 }491 // $FlowFixMe[incompatible-use] found when upgrading Flow492 for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {493 // $FlowFixMe[incompatible-use] found when upgrading Flow494 if (is(nextDeps[i], prevDeps[i])) {495 continue;496 }497 return false;498 }499 return true;500}501502export function renderWithHooks<Props, SecondArg>(503 current: Fiber | null,504 workInProgress: Fiber,505 Component: (p: Props, arg: SecondArg) => any,506 props: Props,507 secondArg: SecondArg,508 nextRenderLanes: Lanes,509): any {510 renderLanes = nextRenderLanes;511 currentlyRenderingFiber = workInProgress;512513 if (__DEV__) {514 hookTypesDev =515 current !== null516 ? (current._debugHookTypes as any as Array<HookType>)517 : null;518 hookTypesUpdateIndexDev = -1;519 // Used for hot reloading:520 ignorePreviousDependencies =521 current !== null && current.type !== workInProgress.type;522523 warnIfAsyncClientComponent(Component);524 }525526 workInProgress.memoizedState = null;527 workInProgress.updateQueue = null;528 workInProgress.lanes = NoLanes;529530 // The following should have already been reset531 // currentHook = null;532 // workInProgressHook = null;533534 // didScheduleRenderPhaseUpdate = false;535 // localIdCounter = 0;536 // thenableIndexCounter = 0;537 // thenableState = null;538539 // TODO Warn if no hooks are used at all during mount, then some are used during update.540 // Currently we will identify the update render as a mount because memoizedState === null.541 // This is tricky because it's valid for certain types of components (e.g. React.lazy)542543 // Using memoizedState to differentiate between mount/update only works if at least one stateful hook is used.544 // Non-stateful hooks (e.g. context) don't get added to memoizedState,545 // so memoizedState would be null during updates and mounts.546 if (__DEV__) {547 if (current !== null && current.memoizedState !== null) {548 ReactSharedInternals.H = HooksDispatcherOnUpdateInDEV;549 } else if (hookTypesDev !== null) {550 // This dispatcher handles an edge case where a component is updating,551 // but no stateful hooks have been used.552 // We want to match the production code behavior (which will use HooksDispatcherOnMount),553 // but with the extra DEV validation to ensure hooks ordering hasn't changed.554 // This dispatcher does that.555 ReactSharedInternals.H = HooksDispatcherOnMountWithHookTypesInDEV;556 } else {557 ReactSharedInternals.H = HooksDispatcherOnMountInDEV;558 }559 } else {560 ReactSharedInternals.H =561 current === null || current.memoizedState === null562 ? HooksDispatcherOnMount563 : HooksDispatcherOnUpdate;564 }565566 // In Strict Mode, during development, user functions are double invoked to567 // help detect side effects. The logic for how this is implemented for in568 // hook components is a bit complex so let's break it down.569 //570 // We will invoke the entire component function twice. However, during the571 // second invocation of the component, the hook state from the first572 // invocation will be reused. That means things like `useMemo` functions won't573 // run again, because the deps will match and the memoized result will574 // be reused.575 //576 // We want memoized functions to run twice, too, so account for this, user577 // functions are double invoked during the *first* invocation of the component578 // function, and are *not* double invoked during the second incovation:579 //580 // - First execution of component function: user functions are double invoked581 // - Second execution of component function (in Strict Mode, during582 // development): user functions are not double invoked.583 //584 // This is intentional for a few reasons; most importantly, it's because of585 // how `use` works when something suspends: it reuses the promise that was586 // passed during the first attempt. This is itself a form of memoization.587 // We need to be able to memoize the reactive inputs to the `use` call using588 // a hook (i.e. `useMemo`), which means, the reactive inputs to `use` must589 // come from the same component invocation as the output.590 //591 // There are plenty of tests to ensure this behavior is correct.592 const shouldDoubleRenderDEV =593 __DEV__ && (workInProgress.mode & StrictLegacyMode) !== NoMode;594595 shouldDoubleInvokeUserFnsInHooksDEV = shouldDoubleRenderDEV;596 let children = __DEV__597 ? callComponentInDEV(Component, props, secondArg)598 : Component(props, secondArg);599 shouldDoubleInvokeUserFnsInHooksDEV = false;600601 // Check if there was a render phase update602 if (didScheduleRenderPhaseUpdateDuringThisPass) {603 // Keep rendering until the component stabilizes (there are no more render604 // phase updates).605 children = renderWithHooksAgain(606 workInProgress,607 Component,608 props,609 secondArg,610 );611 }612613 if (shouldDoubleRenderDEV) {614 // In development, components are invoked twice to help detect side effects.615 setIsStrictModeForDevtools(true);616 try {617 children = renderWithHooksAgain(618 workInProgress,619 Component,620 props,621 secondArg,622 );623 } finally {624 setIsStrictModeForDevtools(false);625 }626 }627628 finishRenderingHooks(current, workInProgress, Component);629630 return children;631}632633function finishRenderingHooks<Props, SecondArg>(634 current: Fiber | null,635 workInProgress: Fiber,636 Component: (p: Props, arg: SecondArg) => any,637): void {638 if (__DEV__) {639 workInProgress._debugHookTypes = hookTypesDev;640 // Stash the thenable state for use by DevTools.641 if (workInProgress.dependencies === null) {642 if (thenableState !== null) {643 workInProgress.dependencies = {644 lanes: NoLanes,645 firstContext: null,646 _debugThenableState: thenableState,647 };648 }649 } else {650 workInProgress.dependencies._debugThenableState = thenableState;651 }652 }653654 // We can assume the previous dispatcher is always this one, since we set it655 // at the beginning of the render phase and there's no re-entrance.656 ReactSharedInternals.H = ContextOnlyDispatcher;657658 // This check uses currentHook so that it works the same in DEV and prod bundles.659 // hookTypesDev could catch more cases (e.g. context) but only in DEV bundles.660 const didRenderTooFewHooks =661 currentHook !== null && currentHook.next !== null;662663 renderLanes = NoLanes;664 currentlyRenderingFiber = null as any;665666 currentHook = null;667 workInProgressHook = null;668669 if (__DEV__) {670 currentHookNameInDev = null;671 hookTypesDev = null;672 hookTypesUpdateIndexDev = -1;673674 // Confirm that a static flag was not added or removed since the last675 // render. If this fires, it suggests that we incorrectly reset the static676 // flags in some other part of the codebase. This has happened before, for677 // example, in the SuspenseList implementation.678 if (679 current !== null &&680 (current.flags & StaticMaskEffect) !==681 (workInProgress.flags & StaticMaskEffect) &&682 // Disable this warning in legacy mode, because legacy Suspense is weird683 // and creates false positives. To make this work in legacy mode, we'd684 // need to mark fibers that commit in an incomplete state, somehow. For685 // now I'll disable the warning that most of the bugs that would trigger686 // it are either exclusive to concurrent mode or exist in both.687 (disableLegacyMode || (current.mode & ConcurrentMode) !== NoMode)688 ) {689 console.error(690 'Internal React error: Expected static flag was missing. Please ' +691 'notify the React team.',692 );693 }694 }695696 didScheduleRenderPhaseUpdate = false;697 // This is reset by checkDidRenderIdHook698 // localIdCounter = 0;699700 thenableIndexCounter = 0;701 thenableState = null;702703 if (didRenderTooFewHooks) {704 throw new Error(705 'Rendered fewer hooks than expected. This may be caused by an accidental ' +706 'early return statement.',707 );708 }709710 if (current !== null) {711 if (!checkIfWorkInProgressReceivedUpdate()) {712 // If there were no changes to props or state, we need to check if there713 // was a context change. We didn't already do this because there's no714 // 1:1 correspondence between dependencies and hooks. Although, because715 // there almost always is in the common case (`readContext` is an716 // internal API), we could compare in there. OTOH, we only hit this case717 // if everything else bails out, so on the whole it might be better to718 // keep the comparison out of the common path.719 const currentDependencies = current.dependencies;720 if (721 currentDependencies !== null &&722 checkIfContextChanged(currentDependencies)723 ) {724 markWorkInProgressReceivedUpdate();725 }726 }727 }728729 if (__DEV__) {730 if (checkIfUseWrappedInTryCatch()) {731 const componentName =732 getComponentNameFromFiber(workInProgress) || 'Unknown';733 if (734 !didWarnAboutUseWrappedInTryCatch.has(componentName) &&735 // This warning also fires if you suspend with `use` inside an736 // async component. Since we warn for that above, we'll silence this737 // second warning by checking here.738 !didWarnAboutAsyncClientComponent.has(componentName)739 ) {740 didWarnAboutUseWrappedInTryCatch.add(componentName);741 console.error(742 '`use` was called from inside a try/catch block. This is not allowed ' +743 'and can lead to unexpected behavior. To handle errors triggered ' +744 'by `use`, wrap your component in a error boundary.',745 );746 }747 }748 }749}750751export function replaySuspendedComponentWithHooks<Props, SecondArg>(752 current: Fiber | null,753 workInProgress: Fiber,754 Component: (p: Props, arg: SecondArg) => any,755 props: Props,756 secondArg: SecondArg,757): any {758 // This function is used to replay a component that previously suspended,759 // after its data resolves.760 //761 // It's a simplified version of renderWithHooks, but it doesn't need to do762 // most of the set up work because they weren't reset when we suspended; they763 // only get reset when the component either completes (finishRenderingHooks)764 // or unwinds (resetHooksOnUnwind).765 if (__DEV__) {766 hookTypesUpdateIndexDev = -1;767 // Used for hot reloading:768 ignorePreviousDependencies =769 current !== null && current.type !== workInProgress.type;770 }771 // renderWithHooks only resets the updateQueue but does not clear it, since772 // it needs to work for both this case (suspense replay) as well as for double773 // renders in dev and setState-in-render. However, for the suspense replay case774 // we need to reset the updateQueue to correctly handle unmount effects, so we775 // clear the queue here776 workInProgress.updateQueue = null;777 const children = renderWithHooksAgain(778 workInProgress,779 Component,780 props,781 secondArg,782 );783 finishRenderingHooks(current, workInProgress, Component);784 return children;785}786787function renderWithHooksAgain<Props, SecondArg>(788 workInProgress: Fiber,789 Component: (p: Props, arg: SecondArg) => any,790 props: Props,791 secondArg: SecondArg,792): any {793 // This is used to perform another render pass. It's used when setState is794 // called during render, and for double invoking components in Strict Mode795 // during development.796 //797 // The state from the previous pass is reused whenever possible. So, state798 // updates that were already processed are not processed again, and memoized799 // functions (`useMemo`) are not invoked again.800 //801 // Keep rendering in a loop for as long as render phase updates continue to802 // be scheduled. Use a counter to prevent infinite loops.803804 currentlyRenderingFiber = workInProgress;805806 let numberOfReRenders: number = 0;807 let children;808 do {809 if (didScheduleRenderPhaseUpdateDuringThisPass) {810 // It's possible that a use() value depended on a state that was updated in811 // this rerender, so we need to watch for different thenables this time.812 thenableState = null;813 }814 thenableIndexCounter = 0;815 didScheduleRenderPhaseUpdateDuringThisPass = false;816817 if (numberOfReRenders >= RE_RENDER_LIMIT) {818 throw new Error(819 'Too many re-renders. React limits the number of renders to prevent ' +820 'an infinite loop.',821 );822 }823824 numberOfReRenders += 1;825 if (__DEV__) {826 // Even when hot reloading, allow dependencies to stabilize827 // after first render to prevent infinite render phase updates.828 ignorePreviousDependencies = false;829 }830831 // Start over from the beginning of the list832 currentHook = null;833 workInProgressHook = null;834835 if (workInProgress.updateQueue != null) {836 resetFunctionComponentUpdateQueue(workInProgress.updateQueue as any);837 }838839 if (__DEV__) {840 // Also validate hook order for cascading updates.841 hookTypesUpdateIndexDev = -1;842 }843844 ReactSharedInternals.H = __DEV__845 ? HooksDispatcherOnRerenderInDEV846 : HooksDispatcherOnRerender;847848 children = __DEV__849 ? callComponentInDEV(Component, props, secondArg)850 : Component(props, secondArg);851 } while (didScheduleRenderPhaseUpdateDuringThisPass);852 return children;853}854855export function renderTransitionAwareHostComponentWithHooks(856 current: Fiber | null,857 workInProgress: Fiber,858 lanes: Lanes,859): TransitionStatus {860 return renderWithHooks(861 current,862 workInProgress,863 TransitionAwareHostComponent,864 null,865 null,866 lanes,867 );868}869870export function TransitionAwareHostComponent(): TransitionStatus {871 const dispatcher: any = ReactSharedInternals.H;872 const [maybeThenable] = dispatcher.useState();873 let nextState;874 if (typeof maybeThenable.then === 'function') {875 const thenable: Thenable<TransitionStatus> = maybeThenable as any;876 nextState = useThenable(thenable);877 } else {878 const status: TransitionStatus = maybeThenable;879 nextState = status;880 }881882 // The "reset state" is an object. If it changes, that means something883 // requested that we reset the form.884 const [nextResetState] = dispatcher.useState();885 const prevResetState =886 currentHook !== null ? currentHook.memoizedState : null;887 if (prevResetState !== nextResetState) {888 // Schedule a form reset889 currentlyRenderingFiber.flags |= FormReset;890 }891892 return nextState;893}894895export function checkDidRenderIdHook(): boolean {896 // This should be called immediately after every renderWithHooks call.897 // Conceptually, it's part of the return value of renderWithHooks; it's only a898 // separate function to avoid using an array tuple.899 const didRenderIdHook = localIdCounter !== 0;900 localIdCounter = 0;901 return didRenderIdHook;902}903904export function bailoutHooks(905 current: Fiber,906 workInProgress: Fiber,907 lanes: Lanes,908): void {909 workInProgress.updateQueue = current.updateQueue;910 // TODO: Don't need to reset the flags here, because they're reset in the911 // complete phase (bubbleProperties).912 if (__DEV__ && (workInProgress.mode & StrictEffectsMode) !== NoMode) {913 workInProgress.flags &= ~(914 MountPassiveDevEffect |915 MountLayoutDevEffect |916 PassiveEffect |917 UpdateEffect918 );919 } else {920 workInProgress.flags &= ~(PassiveEffect | UpdateEffect);921 }922 current.lanes = removeLanes(current.lanes, lanes);923}924925export function resetHooksAfterThrow(): void {926 // This is called immediaetly after a throw. It shouldn't reset the entire927 // module state, because the work loop might decide to replay the component928 // again without rewinding.929 //930 // It should only reset things like the current dispatcher, to prevent hooks931 // from being called outside of a component.932 currentlyRenderingFiber = null as any;933934 // We can assume the previous dispatcher is always this one, since we set it935 // at the beginning of the render phase and there's no re-entrance.936 ReactSharedInternals.H = ContextOnlyDispatcher;937}938939export function resetHooksOnUnwind(workInProgress: Fiber): void {940 if (didScheduleRenderPhaseUpdate) {941 // There were render phase updates. These are only valid for this render942 // phase, which we are now aborting. Remove the updates from the queues so943 // they do not persist to the next render. Do not remove updates from hooks944 // that weren't processed.945 //946 // Only reset the updates from the queue if it has a clone. If it does947 // not have a clone, that means it wasn't processed, and the updates were948 // scheduled before we entered the render phase.949 let hook: Hook | null = workInProgress.memoizedState;950 while (hook !== null) {951 const queue = hook.queue;952 if (queue !== null) {953 queue.pending = null;954 }955 hook = hook.next;956 }957 didScheduleRenderPhaseUpdate = false;958 }959960 renderLanes = NoLanes;961 currentlyRenderingFiber = null as any;962963 currentHook = null;964 workInProgressHook = null;965966 if (__DEV__) {967 hookTypesDev = null;968 hookTypesUpdateIndexDev = -1;969970 currentHookNameInDev = null;971 }972973 didScheduleRenderPhaseUpdateDuringThisPass = false;974 localIdCounter = 0;975 thenableIndexCounter = 0;976 thenableState = null;977}978979function mountWorkInProgressHook(): Hook {980 const hook: Hook = {981 memoizedState: null,982983 baseState: null,984 baseQueue: null,985 queue: null,986987 next: null,988 };989990 if (workInProgressHook === null) {991 // This is the first hook in the list992 currentlyRenderingFiber.memoizedState = workInProgressHook = hook;993 } else {994 // Append to the end of the list995 workInProgressHook = workInProgressHook.next = hook;996 }997 return workInProgressHook;998}9991000function updateWorkInProgressHook(): Hook {1001 // This function is used both for updates and for re-renders triggered by a1002 // render phase update. It assumes there is either a current hook we can1003 // clone, or a work-in-progress hook from a previous render pass that we can1004 // use as a base.1005 let nextCurrentHook: null | Hook;1006 if (currentHook === null) {1007 const current = currentlyRenderingFiber.alternate;1008 if (current !== null) {1009 nextCurrentHook = current.memoizedState;1010 } else {1011 nextCurrentHook = null;1012 }1013 } else {1014 nextCurrentHook = currentHook.next;1015 }10161017 let nextWorkInProgressHook: null | Hook;1018 if (workInProgressHook === null) {1019 nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;1020 } else {1021 nextWorkInProgressHook = workInProgressHook.next;1022 }10231024 if (nextWorkInProgressHook !== null) {1025 // There's already a work-in-progress. Reuse it.1026 workInProgressHook = nextWorkInProgressHook;1027 nextWorkInProgressHook = workInProgressHook.next;10281029 currentHook = nextCurrentHook;1030 } else {1031 // Clone from the current hook.10321033 if (nextCurrentHook === null) {1034 const currentFiber = currentlyRenderingFiber.alternate;1035 if (currentFiber === null) {1036 // This is the initial render. This branch is reached when the component1037 // suspends, resumes, then renders an additional hook.1038 // Should never be reached because we should switch to the mount dispatcher first.1039 throw new Error(1040 'Update hook called on initial render. This is likely a bug in React. Please file an issue.',1041 );1042 } else {1043 // This is an update. We should always have a current hook.1044 throw new Error('Rendered more hooks than during the previous render.');1045 }1046 }10471048 currentHook = nextCurrentHook;10491050 const newHook: Hook = {1051 memoizedState: currentHook.memoizedState,10521053 baseState: currentHook.baseState,1054 baseQueue: currentHook.baseQueue,1055 queue: currentHook.queue,10561057 next: null,1058 };10591060 if (workInProgressHook === null) {1061 // This is the first hook in the list.1062 currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;1063 } else {1064 // Append to the end of the list.1065 workInProgressHook = workInProgressHook.next = newHook;1066 }1067 }1068 return workInProgressHook;1069}10701071function createFunctionComponentUpdateQueue(): FunctionComponentUpdateQueue {1072 return {1073 lastEffect: null,1074 events: null,1075 stores: null,1076 memoCache: null,1077 };1078}10791080function resetFunctionComponentUpdateQueue(1081 updateQueue: FunctionComponentUpdateQueue,1082): void {1083 updateQueue.lastEffect = null;1084 updateQueue.events = null;1085 updateQueue.stores = null;1086 if (updateQueue.memoCache != null) {1087 // NOTE: this function intentionally does not reset memoCache data. We reuse updateQueue for the memo1088 // cache to avoid increasing the size of fibers that don't need a cache, but we don't want to reset1089 // the cache when other properties are reset.1090 updateQueue.memoCache.index = 0;1091 }1092}10931094function useThenable<T>(thenable: Thenable<T>): T {1095 // Track the position of the thenable within this fiber.1096 const index = thenableIndexCounter;1097 thenableIndexCounter += 1;1098 if (thenableState === null) {1099 thenableState = createThenableState();1100 }1101 const result = trackUsedThenable(thenableState, thenable, index);11021103 // When something suspends with `use`, we replay the component with the1104 // "re-render" dispatcher instead of the "mount" or "update" dispatcher.1105 //1106 // But if there are additional hooks that occur after the `use` invocation1107 // that suspended, they wouldn't have been processed during the previous1108 // attempt. So after we invoke `use` again, we may need to switch from the1109 // "re-render" dispatcher back to the "mount" or "update" dispatcher. That's1110 // what the following logic accounts for.1111 //1112 // TODO: Theoretically this logic only needs to go into the rerender1113 // dispatcher. Could optimize, but probably not be worth it.11141115 // This is the same logic as in updateWorkInProgressHook.1116 const workInProgressFiber = currentlyRenderingFiber;1117 const nextWorkInProgressHook =1118 workInProgressHook === null1119 ? // We're at the beginning of the list, so read from the first hook from1120 // the fiber.1121 workInProgressFiber.memoizedState1122 : workInProgressHook.next;11231124 if (nextWorkInProgressHook !== null) {1125 // There are still hooks remaining from the previous attempt.1126 } else {1127 // There are no remaining hooks from the previous attempt. We're no longer1128 // in "re-render" mode. Switch to the normal mount or update dispatcher.1129 //1130 // This is the same as the logic in renderWithHooks, except we don't bother1131 // to track the hook types debug information in this case (sufficient to1132 // only do that when nothing suspends).1133 const currentFiber = workInProgressFiber.alternate;1134 if (__DEV__) {1135 if (currentFiber !== null && currentFiber.memoizedState !== null) {1136 ReactSharedInternals.H = HooksDispatcherOnUpdateInDEV;1137 } else {1138 ReactSharedInternals.H = HooksDispatcherOnMountInDEV;1139 }1140 } else {1141 ReactSharedInternals.H =1142 currentFiber === null || currentFiber.memoizedState === null1143 ? HooksDispatcherOnMount1144 : HooksDispatcherOnUpdate;1145 }1146 }1147 return result;1148}11491150function use<T>(usable: Usable<T>): T {1151 // $FlowFixMe[invalid-compare]1152 if (usable !== null && typeof usable === 'object') {1153 // $FlowFixMe[method-unbinding]1154 if (typeof usable.then === 'function') {1155 // This is a thenable.1156 const thenable: Thenable<T> = usable as any;1157 return useThenable(thenable);1158 } else if (usable.$$typeof === REACT_CONTEXT_TYPE) {1159 const context: ReactContext<T> = usable as any;1160 return readContext(context);1161 }1162 }11631164 // eslint-disable-next-line react-internal/safe-string-coercion1165 throw new Error('An unsupported type was passed to use(): ' + String(usable));1166}11671168function useMemoCache(size: number): Array<mixed> {1169 let memoCache = null;1170 // Fast-path, load memo cache from wip fiber if already prepared1171 let updateQueue: FunctionComponentUpdateQueue | null =1172 currentlyRenderingFiber.updateQueue as any;1173 if (updateQueue !== null) {1174 memoCache = updateQueue.memoCache;1175 }1176 // Otherwise clone from the current fiber1177 if (memoCache == null) {1178 const current: Fiber | null = currentlyRenderingFiber.alternate;1179 if (current !== null) {1180 const currentUpdateQueue: FunctionComponentUpdateQueue | null =1181 current.updateQueue as any;1182 if (currentUpdateQueue !== null) {1183 const currentMemoCache: ?MemoCache = currentUpdateQueue.memoCache;1184 if (currentMemoCache != null) {1185 memoCache = {1186 // When enableNoCloningMemoCache is enabled, instead of treating the1187 // cache as copy-on-write, like we do with fibers, we share the same1188 // cache instance across all render attempts, even if the component1189 // is interrupted before it commits.1190 //1191 // If an update is interrupted, either because it suspended or1192 // because of another update, we can reuse the memoized computations1193 // from the previous attempt. We can do this because the React1194 // Compiler performs atomic writes to the memo cache, i.e. it will1195 // not record the inputs to a memoization without also recording its1196 // output.1197 //1198 // This gives us a form of "resuming" within components and hooks.1199 //1200 // This only works when updating a component that already mounted.1201 // It has no impact during initial render, because the memo cache is1202 // stored on the fiber, and since we have not implemented resuming1203 // for fibers, it's always a fresh memo cache, anyway.1204 //1205 // However, this alone is pretty useful — it happens whenever you1206 // update the UI with fresh data after a mutation/action, which is1207 // extremely common in a Suspense-driven (e.g. RSC or Relay) app.1208 data: enableNoCloningMemoCache1209 ? currentMemoCache.data1210 : // Clone the memo cache before each render (copy-on-write)1211 currentMemoCache.data.map(array => array.slice()),1212 index: 0 as number,1213 };1214 }1215 }1216 }1217 }1218 // Finally fall back to allocating a fresh instance of the cache1219 if (memoCache == null) {1220 memoCache = {1221 data: [],1222 index: 0 as number,1223 };1224 }1225 if (updateQueue === null) {1226 updateQueue = createFunctionComponentUpdateQueue();1227 currentlyRenderingFiber.updateQueue = updateQueue;1228 }1229 updateQueue.memoCache = memoCache;12301231 let data = memoCache.data[memoCache.index];1232 if (data === undefined || (__DEV__ && ignorePreviousDependencies)) {1233 data = memoCache.data[memoCache.index] = new Array(size);1234 for (let i = 0; i < size; i++) {1235 data[i] = REACT_MEMO_CACHE_SENTINEL;1236 }1237 } else if (data.length !== size) {1238 // TODO: consider warning or throwing here1239 if (__DEV__) {1240 console.error(1241 'Expected a constant size argument for each invocation of useMemoCache. ' +1242 'The previous cache was allocated with size %s but size %s was requested.',1243 data.length,1244 size,1245 );1246 }1247 }1248 memoCache.index++;1249 return data;1250}12511252function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {1253 // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types1254 return typeof action === 'function' ? action(state) : action;1255}12561257function mountReducer<S, I, A>(1258 reducer: (S, A) => S,1259 initialArg: I,1260 init?: I => S,1261): [S, Dispatch<A>] {1262 const hook = mountWorkInProgressHook();1263 let initialState;1264 if (init !== undefined) {1265 initialState = init(initialArg);1266 if (shouldDoubleInvokeUserFnsInHooksDEV) {1267 setIsStrictModeForDevtools(true);1268 try {1269 init(initialArg);1270 } finally {1271 setIsStrictModeForDevtools(false);1272 }1273 }1274 } else {1275 initialState = initialArg as any as S;1276 }1277 hook.memoizedState = hook.baseState = initialState;1278 const queue: UpdateQueue<S, A> = {1279 pending: null,1280 lanes: NoLanes,1281 dispatch: null,1282 lastRenderedReducer: reducer,1283 lastRenderedState: initialState as any,1284 };1285 hook.queue = queue;1286 const dispatch: Dispatch<A> = (queue.dispatch = dispatchReducerAction.bind(1287 null,1288 currentlyRenderingFiber,1289 queue,1290 ) as any);1291 return [hook.memoizedState, dispatch];1292}12931294function updateReducer<S, I, A>(1295 reducer: (S, A) => S,1296 initialArg: I,1297 init?: I => S,1298): [S, Dispatch<A>] {1299 const hook = updateWorkInProgressHook();1300 return updateReducerImpl(hook, currentHook as any as Hook, reducer);1301}13021303function updateReducerImpl<S, A>(1304 hook: Hook,1305 current: Hook,1306 reducer: (S, A) => S,1307): [S, Dispatch<A>] {1308 const queue = hook.queue;13091310 if (queue === null) {1311 throw new Error(1312 'Should have a queue. You are likely calling Hooks conditionally, ' +1313 'which is not allowed. (https://react.dev/link/invalid-hook-call)',1314 );1315 }13161317 queue.lastRenderedReducer = reducer;13181319 // The last rebase update that is NOT part of the base state.1320 let baseQueue = hook.baseQueue;13211322 // The last pending update that hasn't been processed yet.1323 const pendingQueue = queue.pending;1324 if (pendingQueue !== null) {1325 // We have new updates that haven't been processed yet.1326 // We'll add them to the base queue.1327 if (baseQueue !== null) {1328 // Merge the pending queue and the base queue.1329 const baseFirst = baseQueue.next;1330 const pendingFirst = pendingQueue.next;1331 baseQueue.next = pendingFirst;1332 pendingQueue.next = baseFirst;1333 }1334 if (__DEV__) {1335 if (current.baseQueue !== baseQueue) {1336 // Internal invariant that should never happen, but feasibly could in1337 // the future if we implement resuming, or some form of that.1338 console.error(1339 'Internal error: Expected work-in-progress queue to be a clone. ' +1340 'This is a bug in React.',1341 );1342 }1343 }1344 current.baseQueue = baseQueue = pendingQueue;1345 queue.pending = null;1346 }13471348 const baseState = hook.baseState;1349 if (baseQueue === null) {1350 // If there are no pending updates, then the memoized state should be the1351 // same as the base state. Currently these only diverge in the case of1352 // useOptimistic, because useOptimistic accepts a new baseState on1353 // every render.1354 hook.memoizedState = baseState;1355 // We don't need to call markWorkInProgressReceivedUpdate because1356 // baseState is derived from other reactive values.1357 } else {1358 // We have a queue to process.1359 const first = baseQueue.next;1360 let newState = baseState;13611362 let newBaseState = null;1363 let newBaseQueueFirst = null;1364 let newBaseQueueLast: Update<S, A> | null = null;1365 let update = first;1366 let didReadFromEntangledAsyncAction = false;1367 do {1368 // An extra OffscreenLane bit is added to updates that were made to1369 // a hidden tree, so that we can distinguish them from updates that were1370 // already there when the tree was hidden.1371 const updateLane = removeLanes(update.lane, OffscreenLane);1372 const isHiddenUpdate = updateLane !== update.lane;13731374 // Check if this update was made while the tree was hidden. If so, then1375 // it's not a "base" update and we should disregard the extra base lanes1376 // that were added to renderLanes when we entered the Offscreen tree.1377 let shouldSkipUpdate = isHiddenUpdate1378 ? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)1379 : !isSubsetOfLanes(renderLanes, updateLane);13801381 if (enableGestureTransition && updateLane === GestureLane) {1382 // This is a gesture optimistic update. It should only be considered as part of the1383 // rendered state while rendering the gesture lane and if the rendering the associated1384 // ScheduledGesture.1385 const scheduledGesture = update.gesture;1386 if (scheduledGesture !== null) {1387 if (scheduledGesture.count === 0 && !scheduledGesture.committing) {1388 // This gesture has already been cancelled. We can clean up this update.1389 update = update.next;1390 continue;1391 } else if (!isGestureRender(renderLanes)) {1392 shouldSkipUpdate = true;1393 } else {1394 const root: FiberRoot | null = getWorkInProgressRoot();1395 if (root === null) {1396 throw new Error(1397 'Expected a work-in-progress root. This is a bug in React. Please file an issue.',1398 );1399 }1400 // We assume that the currently rendering gesture is the one first in the queue.1401 shouldSkipUpdate = root.pendingGestures !== scheduledGesture;1402 }1403 }1404 }14051406 if (shouldSkipUpdate) {1407 // Priority is insufficient. Skip this update. If this is the first1408 // skipped update, the previous update/state is the new base1409 // update/state.1410 const clone: Update<S, A> = {1411 lane: updateLane,1412 revertLane: update.revertLane,1413 gesture: update.gesture,1414 action: update.action,1415 hasEagerState: update.hasEagerState,1416 eagerState: update.eagerState,1417 next: null as any,1418 };1419 if (newBaseQueueLast === null) {1420 newBaseQueueFirst = newBaseQueueLast = clone;1421 newBaseState = newState;1422 } else {1423 newBaseQueueLast = newBaseQueueLast.next = clone;1424 }1425 // Update the remaining priority in the queue.1426 // TODO: Don't need to accumulate this. Instead, we can remove1427 // renderLanes from the original lanes.1428 currentlyRenderingFiber.lanes = mergeLanes(1429 currentlyRenderingFiber.lanes,1430 updateLane,1431 );1432 markSkippedUpdateLanes(updateLane);1433 } else {1434 // This update does have sufficient priority.14351436 // Check if this is an optimistic update.1437 const revertLane = update.revertLane;1438 if (revertLane === NoLane) {1439 // This is not an optimistic update, and we're going to apply it now.1440 // But, if there were earlier updates that were skipped, we need to1441 // leave this update in the queue so it can be rebased later.1442 if (newBaseQueueLast !== null) {1443 const clone: Update<S, A> = {1444 // This update is going to be committed so we never want uncommit1445 // it. Using NoLane works because 0 is a subset of all bitmasks, so1446 // this will never be skipped by the check above.1447 lane: NoLane,1448 revertLane: NoLane,1449 gesture: null,1450 action: update.action,1451 hasEagerState: update.hasEagerState,1452 eagerState: update.eagerState,1453 next: null as any,1454 };1455 newBaseQueueLast = newBaseQueueLast.next = clone;1456 }14571458 // Check if this update is part of a pending async action. If so,1459 // we'll need to suspend until the action has finished, so that it's1460 // batched together with future updates in the same action.1461 if (updateLane === peekEntangledActionLane()) {1462 didReadFromEntangledAsyncAction = true;1463 }1464 } else {1465 // This is an optimistic update. If the "revert" priority is1466 // sufficient, don't apply the update. Otherwise, apply the update,1467 // but leave it in the queue so it can be either reverted or1468 // rebased in a subsequent render.1469 if (isSubsetOfLanes(renderLanes, revertLane)) {1470 // The transition that this optimistic update is associated with1471 // has finished. Pretend the update doesn't exist by skipping1472 // over it.1473 update = update.next;14741475 // Check if this update is part of a pending async action. If so,1476 // we'll need to suspend until the action has finished, so that it's1477 // batched together with future updates in the same action.1478 if (revertLane === peekEntangledActionLane()) {1479 didReadFromEntangledAsyncAction = true;1480 }1481 continue;1482 } else {1483 const clone: Update<S, A> = {1484 // Once we commit an optimistic update, we shouldn't uncommit it1485 // until the transition it is associated with has finished1486 // (represented by revertLane). Using NoLane here works because 01487 // is a subset of all bitmasks, so this will never be skipped by1488 // the check above.1489 lane: NoLane,1490 // Reuse the same revertLane so we know when the transition1491 // has finished.1492 revertLane: update.revertLane,1493 gesture: null, // If it commits, it's no longer a gesture update.1494 action: update.action,1495 hasEagerState: update.hasEagerState,1496 eagerState: update.eagerState,1497 next: null as any,1498 };1499 if (newBaseQueueLast === null) {1500 newBaseQueueFirst = newBaseQueueLast = clone;1501 newBaseState = newState;1502 } else {1503 newBaseQueueLast = newBaseQueueLast.next = clone;1504 }1505 // Update the remaining priority in the queue.1506 // TODO: Don't need to accumulate this. Instead, we can remove1507 // renderLanes from the original lanes.1508 currentlyRenderingFiber.lanes = mergeLanes(1509 currentlyRenderingFiber.lanes,1510 revertLane,1511 );1512 markSkippedUpdateLanes(revertLane);1513 }1514 }15151516 // Process this update.1517 const action = update.action;1518 if (shouldDoubleInvokeUserFnsInHooksDEV) {1519 reducer(newState, action);1520 }1521 if (update.hasEagerState) {1522 // If this update is a state update (not a reducer) and was processed eagerly,1523 // we can use the eagerly computed state1524 newState = update.eagerState as any as S;1525 } else {1526 newState = reducer(newState, action);1527 }1528 }1529 update = update.next;1530 // $FlowFixMe[invalid-compare]1531 } while (update !== null && update !== first);15321533 if (newBaseQueueLast === null) {1534 newBaseState = newState;1535 } else {1536 newBaseQueueLast.next = newBaseQueueFirst as any;1537 }15381539 // Mark that the fiber performed work, but only if the new state is1540 // different from the current state.1541 if (!is(newState, hook.memoizedState)) {1542 markWorkInProgressReceivedUpdate();15431544 // Check if this update is part of a pending async action. If so, we'll1545 // need to suspend until the action has finished, so that it's batched1546 // together with future updates in the same action.1547 // TODO: Once we support hooks inside useMemo (or an equivalent1548 // memoization boundary like Forget), hoist this logic so that it only1549 // suspends if the memo boundary produces a new value.1550 if (didReadFromEntangledAsyncAction) {1551 const entangledActionThenable = peekEntangledActionThenable();1552 if (entangledActionThenable !== null) {1553 // TODO: Instead of the throwing the thenable directly, throw a1554 // special object like `use` does so we can detect if it's captured1555 // by userspace.1556 throw entangledActionThenable;1557 }1558 }1559 }15601561 hook.memoizedState = newState;1562 hook.baseState = newBaseState;1563 hook.baseQueue = newBaseQueueLast;15641565 queue.lastRenderedState = newState;1566 }15671568 if (baseQueue === null) {1569 // `queue.lanes` is used for entangling transitions. We can set it back to1570 // zero once the queue is empty.1571 queue.lanes = NoLanes;1572 }15731574 const dispatch: Dispatch<A> = queue.dispatch as any;1575 return [hook.memoizedState, dispatch];1576}15771578function rerenderReducer<S, I, A>(1579 reducer: (S, A) => S,1580 initialArg: I,1581 init?: I => S,1582): [S, Dispatch<A>] {1583 const hook = updateWorkInProgressHook();1584 const queue = hook.queue;15851586 if (queue === null) {1587 throw new Error(1588 'Should have a queue. You are likely calling Hooks conditionally, ' +1589 'which is not allowed. (https://react.dev/link/invalid-hook-call)',1590 );1591 }15921593 queue.lastRenderedReducer = reducer;15941595 // This is a re-render. Apply the new render phase updates to the previous1596 // work-in-progress hook.1597 const dispatch: Dispatch<A> = queue.dispatch as any;1598 const lastRenderPhaseUpdate = queue.pending;1599 let newState = hook.memoizedState;1600 if (lastRenderPhaseUpdate !== null) {1601 // The queue doesn't persist past this render pass.1602 queue.pending = null;16031604 const firstRenderPhaseUpdate = lastRenderPhaseUpdate.next;1605 let update = firstRenderPhaseUpdate;1606 do {1607 // Process this render phase update. We don't have to check the1608 // priority because it will always be the same as the current1609 // render's.1610 const action = update.action;1611 newState = reducer(newState, action);1612 update = update.next;1613 } while (update !== firstRenderPhaseUpdate);16141615 // Mark that the fiber performed work, but only if the new state is1616 // different from the current state.1617 if (!is(newState, hook.memoizedState)) {1618 markWorkInProgressReceivedUpdate();1619 }16201621 hook.memoizedState = newState;1622 // Don't persist the state accumulated from the render phase updates to1623 // the base state unless the queue is empty.1624 // TODO: Not sure if this is the desired semantics, but it's what we1625 // do for gDSFP. I can't remember why.1626 if (hook.baseQueue === null) {1627 hook.baseState = newState;1628 }16291630 queue.lastRenderedState = newState;1631 }1632 return [newState, dispatch];1633}16341635function mountSyncExternalStore<T>(1636 subscribe: (() => void) => () => void,1637 getSnapshot: () => T,1638 getServerSnapshot?: () => T,1639): T {1640 const fiber = currentlyRenderingFiber;1641 const hook = mountWorkInProgressHook();16421643 let nextSnapshot;1644 const isHydrating = getIsHydrating();1645 if (isHydrating) {1646 if (getServerSnapshot === undefined) {1647 throw new Error(1648 'Missing getServerSnapshot, which is required for ' +1649 'server-rendered content. Will revert to client rendering.',1650 );1651 }1652 nextSnapshot = getServerSnapshot();1653 if (__DEV__) {1654 if (!didWarnUncachedGetSnapshot) {1655 if (nextSnapshot !== getServerSnapshot()) {1656 console.error(1657 'The result of getServerSnapshot should be cached to avoid an infinite loop',1658 );1659 didWarnUncachedGetSnapshot = true;1660 }1661 }1662 }1663 } else {1664 nextSnapshot = getSnapshot();1665 if (__DEV__) {1666 if (!didWarnUncachedGetSnapshot) {1667 const cachedSnapshot = getSnapshot();1668 if (!is(nextSnapshot, cachedSnapshot)) {1669 console.error(1670 'The result of getSnapshot should be cached to avoid an infinite loop',1671 );1672 didWarnUncachedGetSnapshot = true;1673 }1674 }1675 }1676 // Unless we're rendering a blocking lane, schedule a consistency check.1677 // Right before committing, we will walk the tree and check if any of the1678 // stores were mutated.1679 //1680 // We won't do this if we're hydrating server-rendered content, because if1681 // the content is stale, it's already visible anyway. Instead we'll patch1682 // it up in a passive effect.1683 const root: FiberRoot | null = getWorkInProgressRoot();16841685 if (root === null) {1686 throw new Error(1687 'Expected a work-in-progress root. This is a bug in React. Please file an issue.',1688 );1689 }16901691 const rootRenderLanes = getWorkInProgressRootRenderLanes();1692 if (!includesBlockingLane(rootRenderLanes)) {1693 pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot);1694 }1695 }16961697 // Read the current snapshot from the store on every render. This breaks the1698 // normal rules of React, and only works because store updates are1699 // always synchronous.1700 hook.memoizedState = nextSnapshot;1701 const inst: StoreInstance<T> = {1702 value: nextSnapshot,1703 getSnapshot,1704 };1705 hook.queue = inst;17061707 // Schedule an effect to subscribe to the store.1708 mountEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [subscribe]);17091710 // Schedule an effect to update the mutable instance fields. We will update1711 // this whenever subscribe, getSnapshot, or value changes. Because there's no1712 // clean-up function, and we track the deps correctly, we can call pushEffect1713 // directly, without storing any additional state. For the same reason, we1714 // don't need to set a static flag, either.1715 fiber.flags |= PassiveEffect;1716 pushSimpleEffect(1717 HookHasEffect | HookPassive,1718 createEffectInstance(),1719 updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot),1720 null,1721 );17221723 return nextSnapshot;1724}17251726function updateSyncExternalStore<T>(1727 subscribe: (() => void) => () => void,1728 getSnapshot: () => T,1729 getServerSnapshot?: () => T,1730): T {1731 const fiber = currentlyRenderingFiber;1732 const hook = updateWorkInProgressHook();1733 // Read the current snapshot from the store on every render. This breaks the1734 // normal rules of React, and only works because store updates are1735 // always synchronous.1736 let nextSnapshot;1737 const isHydrating = getIsHydrating();1738 if (isHydrating) {1739 // Needed for strict mode double render1740 if (getServerSnapshot === undefined) {1741 throw new Error(1742 'Missing getServerSnapshot, which is required for ' +1743 'server-rendered content. Will revert to client rendering.',1744 );1745 }1746 nextSnapshot = getServerSnapshot();1747 } else {1748 nextSnapshot = getSnapshot();1749 if (__DEV__) {1750 if (!didWarnUncachedGetSnapshot) {1751 const cachedSnapshot = getSnapshot();1752 if (!is(nextSnapshot, cachedSnapshot)) {1753 console.error(1754 'The result of getSnapshot should be cached to avoid an infinite loop',1755 );1756 didWarnUncachedGetSnapshot = true;1757 }1758 }1759 }1760 }1761 const prevSnapshot = (currentHook || hook).memoizedState;1762 const snapshotChanged = !is(prevSnapshot, nextSnapshot);1763 if (snapshotChanged) {1764 hook.memoizedState = nextSnapshot;1765 markWorkInProgressReceivedUpdate();1766 }1767 const inst = hook.queue;17681769 updateEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [1770 subscribe,1771 ]);17721773 // Whenever getSnapshot or subscribe changes, we need to check in the1774 // commit phase if there was an interleaved mutation. In concurrent mode1775 // this can happen all the time, but even in synchronous mode, an earlier1776 // effect may have mutated the store.1777 if (1778 inst.getSnapshot !== getSnapshot ||1779 snapshotChanged ||1780 // Check if the subscribe function changed. We can save some memory by1781 // checking whether we scheduled a subscription effect above.1782 (workInProgressHook !== null &&1783 workInProgressHook.memoizedState.tag & HookHasEffect)1784 ) {1785 fiber.flags |= PassiveEffect;1786 pushSimpleEffect(1787 HookHasEffect | HookPassive,1788 createEffectInstance(),1789 updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot),1790 null,1791 );17921793 // Unless we're rendering a blocking lane, schedule a consistency check.1794 // Right before committing, we will walk the tree and check if any of the1795 // stores were mutated.1796 const root: FiberRoot | null = getWorkInProgressRoot();17971798 if (root === null) {1799 throw new Error(1800 'Expected a work-in-progress root. This is a bug in React. Please file an issue.',1801 );1802 }18031804 if (!isHydrating && !includesBlockingLane(renderLanes)) {1805 pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot);1806 }1807 }18081809 return nextSnapshot;1810}18111812function pushStoreConsistencyCheck<T>(1813 fiber: Fiber,1814 getSnapshot: () => T,1815 renderedSnapshot: T,1816): void {1817 fiber.flags |= StoreConsistency;1818 const check: StoreConsistencyCheck<T> = {1819 getSnapshot,1820 value: renderedSnapshot,1821 };1822 let componentUpdateQueue: null | FunctionComponentUpdateQueue =1823 currentlyRenderingFiber.updateQueue as any;1824 if (componentUpdateQueue === null) {1825 componentUpdateQueue = createFunctionComponentUpdateQueue();1826 currentlyRenderingFiber.updateQueue = componentUpdateQueue as any;1827 componentUpdateQueue.stores = [check];1828 } else {1829 const stores = componentUpdateQueue.stores;1830 if (stores === null) {1831 componentUpdateQueue.stores = [check];1832 } else {1833 stores.push(check);1834 }1835 }1836}18371838function updateStoreInstance<T>(1839 fiber: Fiber,1840 inst: StoreInstance<T>,1841 nextSnapshot: T,1842 getSnapshot: () => T,1843): void {1844 // These are updated in the passive phase1845 inst.value = nextSnapshot;1846 inst.getSnapshot = getSnapshot;18471848 // Something may have been mutated in between render and commit. This could1849 // have been in an event that fired before the passive effects, or it could1850 // have been in a layout effect. In that case, we would have used the old1851 // snapsho and getSnapshot values to bail out. We need to check one more time.1852 if (checkIfSnapshotChanged(inst)) {1853 // Force a re-render.1854 // We intentionally don't log update times and stacks here because this1855 // was not an external trigger but rather an internal one.1856 forceStoreRerender(fiber);1857 }1858}18591860function subscribeToStore<T>(1861 fiber: Fiber,1862 inst: StoreInstance<T>,1863 subscribe: (() => void) => () => void,1864): any {1865 const handleStoreChange = () => {1866 // The store changed. Check if the snapshot changed since the last time we1867 // read from the store.1868 if (checkIfSnapshotChanged(inst)) {1869 // Force a re-render.1870 startUpdateTimerByLane(SyncLane, 'updateSyncExternalStore()', fiber);1871 forceStoreRerender(fiber);1872 }1873 };1874 // Subscribe to the store and return a clean-up function.1875 return subscribe(handleStoreChange);1876}18771878function checkIfSnapshotChanged<T>(inst: StoreInstance<T>): boolean {1879 const latestGetSnapshot = inst.getSnapshot;1880 const prevValue = inst.value;1881 try {1882 const nextValue = latestGetSnapshot();1883 return !is(prevValue, nextValue);1884 } catch (error) {1885 return true;1886 }1887}18881889function forceStoreRerender(fiber: Fiber) {1890 const root = enqueueConcurrentRenderForLane(fiber, SyncLane);1891 if (root !== null) {1892 scheduleUpdateOnFiber(root, fiber, SyncLane);1893 }1894}18951896function mountStateImpl<S>(initialState: (() => S) | S): Hook {1897 const hook = mountWorkInProgressHook();1898 if (typeof initialState === 'function') {1899 const initialStateInitializer = initialState;1900 // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types1901 initialState = initialStateInitializer();1902 if (shouldDoubleInvokeUserFnsInHooksDEV) {1903 setIsStrictModeForDevtools(true);1904 try {1905 // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types1906 initialStateInitializer();1907 } finally {1908 setIsStrictModeForDevtools(false);1909 }1910 }1911 }1912 hook.memoizedState = hook.baseState = initialState;1913 const queue: UpdateQueue<S, BasicStateAction<S>> = {1914 pending: null,1915 lanes: NoLanes,1916 dispatch: null,1917 lastRenderedReducer: basicStateReducer,1918 lastRenderedState: initialState as any,1919 };1920 hook.queue = queue;1921 return hook;1922}19231924function mountState<S>(1925 initialState: (() => S) | S,1926): [S, Dispatch<BasicStateAction<S>>] {1927 const hook = mountStateImpl(initialState);1928 const queue = hook.queue;1929 const dispatch: Dispatch<BasicStateAction<S>> = dispatchSetState.bind(1930 null,1931 currentlyRenderingFiber,1932 queue,1933 ) as any;1934 queue.dispatch = dispatch;1935 return [hook.memoizedState, dispatch];1936}19371938function updateState<S>(1939 initialState: (() => S) | S,1940): [S, Dispatch<BasicStateAction<S>>] {1941 return updateReducer(basicStateReducer, initialState);1942}19431944function rerenderState<S>(1945 initialState: (() => S) | S,1946): [S, Dispatch<BasicStateAction<S>>] {1947 return rerenderReducer(basicStateReducer, initialState);1948}19491950function mountOptimistic<S, A>(1951 passthrough: S,1952 reducer: ?(S, A) => S,1953): [S, (A) => void] {1954 const hook = mountWorkInProgressHook();1955 hook.memoizedState = hook.baseState = passthrough;1956 const queue: UpdateQueue<S, A> = {1957 pending: null,1958 lanes: NoLanes,1959 dispatch: null,1960 // Optimistic state does not use the eager update optimization.1961 lastRenderedReducer: null,1962 lastRenderedState: null,1963 };1964 hook.queue = queue;1965 // This is different than the normal setState function.1966 const dispatch: A => void = dispatchOptimisticSetState.bind(1967 null,1968 currentlyRenderingFiber,1969 true,1970 queue,1971 ) as any;1972 queue.dispatch = dispatch;1973 return [passthrough, dispatch];1974}19751976function updateOptimistic<S, A>(1977 passthrough: S,1978 reducer: ?(S, A) => S,1979): [S, (A) => void] {1980 const hook = updateWorkInProgressHook();1981 return updateOptimisticImpl(1982 hook,1983 currentHook as any as Hook,1984 passthrough,1985 reducer,1986 );1987}19881989function updateOptimisticImpl<S, A>(1990 hook: Hook,1991 current: Hook | null,1992 passthrough: S,1993 reducer: ?(S, A) => S,1994): [S, (A) => void] {1995 // Optimistic updates are always rebased on top of the latest value passed in1996 // as an argument. It's called a passthrough because if there are no pending1997 // updates, it will be returned as-is.1998 //1999 // Reset the base state to the passthrough. Future updates will be applied2000 // on top of this.
Findings
✓ No findings reported for this file.