packages/react-reconciler/src/ReactFiberClassUpdateQueue.js JAVASCRIPT 766 lines View on github.com → Search inside
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 */910// UpdateQueue is a linked list of prioritized updates.11//12// Like fibers, update queues come in pairs: a current queue, which represents13// the visible state of the screen, and a work-in-progress queue, which can be14// mutated and processed asynchronously before it is committed — a form of15// double buffering. If a work-in-progress render is discarded before finishing,16// we create a new work-in-progress by cloning the current queue.17//18// Both queues share a persistent, singly-linked list structure. To schedule an19// update, we append it to the end of both queues. Each queue maintains a20// pointer to first update in the persistent list that hasn't been processed.21// The work-in-progress pointer always has a position equal to or greater than22// the current queue, since we always work on that one. The current queue's23// pointer is only updated during the commit phase, when we swap in the24// work-in-progress.25//26// For example:27//28//   Current pointer:           A - B - C - D - E - F29//   Work-in-progress pointer:              D - E - F30//                                          ^31//                                          The work-in-progress queue has32//                                          processed more updates than current.33//34// The reason we append to both queues is because otherwise we might drop35// updates without ever processing them. For example, if we only add updates to36// the work-in-progress queue, some updates could be lost whenever a work-in37// -progress render restarts by cloning from current. Similarly, if we only add38// updates to the current queue, the updates will be lost whenever an already39// in-progress queue commits and swaps with the current queue. However, by40// adding to both queues, we guarantee that the update will be part of the next41// work-in-progress. (And because the work-in-progress queue becomes the42// current queue once it commits, there's no danger of applying the same43// update twice.)44//45// Prioritization46// --------------47//48// Updates are not sorted by priority, but by insertion; new updates are always49// appended to the end of the list.50//51// The priority is still important, though. When processing the update queue52// during the render phase, only the updates with sufficient priority are53// included in the result. If we skip an update because it has insufficient54// priority, it remains in the queue to be processed later, during a lower55// priority render. Crucially, all updates subsequent to a skipped update also56// remain in the queue *regardless of their priority*. That means high priority57// updates are sometimes processed twice, at two separate priorities. We also58// keep track of a base state, that represents the state before the first59// update in the queue is applied.60//61// For example:62//63//   Given a base state of '', and the following queue of updates64//65//     A1 - B2 - C1 - D266//67//   where the number indicates the priority, and the update is applied to the68//   previous state by appending a letter, React will process these updates as69//   two separate renders, one per distinct priority level:70//71//   First render, at priority 1:72//     Base state: ''73//     Updates: [A1, C1]74//     Result state: 'AC'75//76//   Second render, at priority 2:77//     Base state: 'A'            <-  The base state does not include C1,78//                                    because B2 was skipped.79//     Updates: [B2, C1, D2]      <-  C1 was rebased on top of B280//     Result state: 'ABCD'81//82// Because we process updates in insertion order, and rebase high priority83// updates when preceding updates are skipped, the final result is deterministic84// regardless of priority. Intermediate state may vary according to system85// resources, but the final state is always the same.8687import type {Fiber, FiberRoot} from './ReactInternalTypes';88import type {Lanes, Lane} from './ReactFiberLane';8990import {91  NoLane,92  NoLanes,93  OffscreenLane,94  isSubsetOfLanes,95  mergeLanes,96  removeLanes,97  isTransitionLane,98  intersectLanes,99  markRootEntangled,100} from './ReactFiberLane';101import {102  enterDisallowedContextReadInDEV,103  exitDisallowedContextReadInDEV,104} from './ReactFiberNewContext';105import {106  Callback,107  Visibility,108  ShouldCapture,109  DidCapture,110} from './ReactFiberFlags';111import getComponentNameFromFiber from './getComponentNameFromFiber';112113import {StrictLegacyMode} from './ReactTypeOfMode';114import {115  markSkippedUpdateLanes,116  isUnsafeClassRenderPhaseUpdate,117  getWorkInProgressRootRenderLanes,118} from './ReactFiberWorkLoop';119import {120  enqueueConcurrentClassUpdate,121  unsafe_markUpdateLaneFromFiberToRoot,122} from './ReactFiberConcurrentUpdates';123import {setIsStrictModeForDevtools} from './ReactFiberDevToolsHook';124125import assign from 'shared/assign';126import {127  peekEntangledActionLane,128  peekEntangledActionThenable,129} from './ReactFiberAsyncAction';130131export type Update<State> = {132  lane: Lane,133134  tag: 0 | 1 | 2 | 3,135  payload: any,136  callback: (() => mixed) | null,137138  next: Update<State> | null,139};140141export type SharedQueue<State> = {142  pending: Update<State> | null,143  lanes: Lanes,144  hiddenCallbacks: Array<() => mixed> | null,145};146147export type UpdateQueue<State> = {148  baseState: State,149  firstBaseUpdate: Update<State> | null,150  lastBaseUpdate: Update<State> | null,151  shared: SharedQueue<State>,152  callbacks: Array<() => mixed> | null,153};154155export const UpdateState = 0;156export const ReplaceState = 1;157export const ForceUpdate = 2;158export const CaptureUpdate = 3;159160// Global state that is reset at the beginning of calling `processUpdateQueue`.161// It should only be read right after calling `processUpdateQueue`, via162// `checkHasForceUpdateAfterProcessing`.163let hasForceUpdate = false;164165let didWarnUpdateInsideUpdate;166let currentlyProcessingQueue: ?SharedQueue<$FlowFixMe>;167export let resetCurrentlyProcessingQueue: () => void;168if (__DEV__) {169  didWarnUpdateInsideUpdate = false;170  currentlyProcessingQueue = null;171  resetCurrentlyProcessingQueue = () => {172    currentlyProcessingQueue = null;173  };174}175176export function initializeUpdateQueue<State>(fiber: Fiber): void {177  const queue: UpdateQueue<State> = {178    baseState: fiber.memoizedState,179    firstBaseUpdate: null,180    lastBaseUpdate: null,181    shared: {182      pending: null,183      lanes: NoLanes,184      hiddenCallbacks: null,185    },186    callbacks: null,187  };188  fiber.updateQueue = queue;189}190191export function cloneUpdateQueue<State>(192  current: Fiber,193  workInProgress: Fiber,194): void {195  // Clone the update queue from current. Unless it's already a clone.196  const queue: UpdateQueue<State> = workInProgress.updateQueue as any;197  const currentQueue: UpdateQueue<State> = current.updateQueue as any;198  if (queue === currentQueue) {199    const clone: UpdateQueue<State> = {200      baseState: currentQueue.baseState,201      firstBaseUpdate: currentQueue.firstBaseUpdate,202      lastBaseUpdate: currentQueue.lastBaseUpdate,203      shared: currentQueue.shared,204      callbacks: null,205    };206    workInProgress.updateQueue = clone;207  }208}209210export function createUpdate(lane: Lane): Update<mixed> {211  const update: Update<mixed> = {212    lane,213214    tag: UpdateState,215    payload: null,216    callback: null,217218    next: null,219  };220  return update;221}222223export function enqueueUpdate<State>(224  fiber: Fiber,225  update: Update<State>,226  lane: Lane,227): FiberRoot | null {228  const updateQueue = fiber.updateQueue;229  if (updateQueue === null) {230    // Only occurs if the fiber has been unmounted.231    return null;232  }233234  const sharedQueue: SharedQueue<State> = (updateQueue as any).shared;235236  if (__DEV__) {237    if (238      currentlyProcessingQueue === sharedQueue &&239      !didWarnUpdateInsideUpdate240    ) {241      const componentName = getComponentNameFromFiber(fiber);242      console.error(243        'An update (setState, replaceState, or forceUpdate) was scheduled ' +244          'from inside an update function. Update functions should be pure, ' +245          'with zero side-effects. Consider using componentDidUpdate or a ' +246          'callback.\n\nPlease update the following component: %s',247        componentName,248      );249      didWarnUpdateInsideUpdate = true;250    }251  }252253  if (isUnsafeClassRenderPhaseUpdate(fiber)) {254    // This is an unsafe render phase update. Add directly to the update255    // queue so we can process it immediately during the current render.256    const pending = sharedQueue.pending;257    if (pending === null) {258      // This is the first update. Create a circular list.259      update.next = update;260    } else {261      update.next = pending.next;262      pending.next = update;263    }264    sharedQueue.pending = update;265266    // Update the childLanes even though we're most likely already rendering267    // this fiber. This is for backwards compatibility in the case where you268    // update a different component during render phase than the one that is269    // currently renderings (a pattern that is accompanied by a warning).270    return unsafe_markUpdateLaneFromFiberToRoot(fiber, lane);271  } else {272    return enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane);273  }274}275276export function entangleTransitions(root: FiberRoot, fiber: Fiber, lane: Lane) {277  const updateQueue = fiber.updateQueue;278  if (updateQueue === null) {279    // Only occurs if the fiber has been unmounted.280    return;281  }282283  const sharedQueue: SharedQueue<mixed> = (updateQueue as any).shared;284  if (isTransitionLane(lane)) {285    let queueLanes = sharedQueue.lanes;286287    // If any entangled lanes are no longer pending on the root, then they must288    // have finished. We can remove them from the shared queue, which represents289    // a superset of the actually pending lanes. In some cases we may entangle290    // more than we need to, but that's OK. In fact it's worse if we *don't*291    // entangle when we should.292    queueLanes = intersectLanes(queueLanes, root.pendingLanes);293294    // Entangle the new transition lane with the other transition lanes.295    const newQueueLanes = mergeLanes(queueLanes, lane);296    sharedQueue.lanes = newQueueLanes;297    // Even if queue.lanes already include lane, we don't know for certain if298    // the lane finished since the last time we entangled it. So we need to299    // entangle it again, just to be sure.300    markRootEntangled(root, newQueueLanes);301  }302}303304export function enqueueCapturedUpdate<State>(305  workInProgress: Fiber,306  capturedUpdate: Update<State>,307) {308  // Captured updates are updates that are thrown by a child during the render309  // phase. They should be discarded if the render is aborted. Therefore,310  // we should only put them on the work-in-progress queue, not the current one.311  let queue: UpdateQueue<State> = workInProgress.updateQueue as any;312313  // Check if the work-in-progress queue is a clone.314  const current = workInProgress.alternate;315  if (current !== null) {316    const currentQueue: UpdateQueue<State> = current.updateQueue as any;317    if (queue === currentQueue) {318      // The work-in-progress queue is the same as current. This happens when319      // we bail out on a parent fiber that then captures an error thrown by320      // a child. Since we want to append the update only to the work-in321      // -progress queue, we need to clone the updates. We usually clone during322      // processUpdateQueue, but that didn't happen in this case because we323      // skipped over the parent when we bailed out.324      let newFirst = null;325      let newLast = null;326      const firstBaseUpdate = queue.firstBaseUpdate;327      if (firstBaseUpdate !== null) {328        // Loop through the updates and clone them.329        let update: Update<State> = firstBaseUpdate;330        do {331          const clone: Update<State> = {332            lane: update.lane,333334            tag: update.tag,335            payload: update.payload,336            // When this update is rebased, we should not fire its337            // callback again.338            callback: null,339340            next: null,341          };342          if (newLast === null) {343            newFirst = newLast = clone;344          } else {345            newLast.next = clone;346            newLast = clone;347          }348          // $FlowFixMe[incompatible-type] we bail out when we get a null349          update = update.next;350        } while (update !== null);351352        // Append the captured update the end of the cloned list.353        // $FlowFixMe[invalid-compare]354        if (newLast === null) {355          newFirst = newLast = capturedUpdate;356        } else {357          newLast.next = capturedUpdate;358          newLast = capturedUpdate;359        }360      } else {361        // There are no base updates.362        newFirst = newLast = capturedUpdate;363      }364      queue = {365        baseState: currentQueue.baseState,366        firstBaseUpdate: newFirst,367        lastBaseUpdate: newLast,368        shared: currentQueue.shared,369        callbacks: currentQueue.callbacks,370      };371      workInProgress.updateQueue = queue;372      return;373    }374  }375376  // Append the update to the end of the list.377  const lastBaseUpdate = queue.lastBaseUpdate;378  if (lastBaseUpdate === null) {379    queue.firstBaseUpdate = capturedUpdate;380  } else {381    lastBaseUpdate.next = capturedUpdate;382  }383  queue.lastBaseUpdate = capturedUpdate;384}385386function getStateFromUpdate<State>(387  workInProgress: Fiber,388  queue: UpdateQueue<State>,389  update: Update<State>,390  prevState: State,391  nextProps: any,392  instance: any,393): any {394  switch (update.tag) {395    case ReplaceState: {396      const payload = update.payload;397      if (typeof payload === 'function') {398        // Updater function399        if (__DEV__) {400          enterDisallowedContextReadInDEV();401        }402        const nextState = payload.call(instance, prevState, nextProps);403        if (__DEV__) {404          if (workInProgress.mode & StrictLegacyMode) {405            setIsStrictModeForDevtools(true);406            try {407              payload.call(instance, prevState, nextProps);408            } finally {409              setIsStrictModeForDevtools(false);410            }411          }412          exitDisallowedContextReadInDEV();413        }414        return nextState;415      }416      // State object417      return payload;418    }419    case CaptureUpdate: {420      workInProgress.flags =421        (workInProgress.flags & ~ShouldCapture) | DidCapture;422    }423    // Intentional fallthrough424    case UpdateState: {425      const payload = update.payload;426      let partialState;427      if (typeof payload === 'function') {428        // Updater function429        if (__DEV__) {430          enterDisallowedContextReadInDEV();431        }432        partialState = payload.call(instance, prevState, nextProps);433        if (__DEV__) {434          if (workInProgress.mode & StrictLegacyMode) {435            setIsStrictModeForDevtools(true);436            try {437              payload.call(instance, prevState, nextProps);438            } finally {439              setIsStrictModeForDevtools(false);440            }441          }442          exitDisallowedContextReadInDEV();443        }444      } else {445        // Partial state object446        partialState = payload;447      }448      if (partialState === null || partialState === undefined) {449        // Null and undefined are treated as no-ops.450        return prevState;451      }452      // Merge the partial state and the previous state.453      return assign({}, prevState, partialState);454    }455    case ForceUpdate: {456      hasForceUpdate = true;457      return prevState;458    }459  }460  return prevState;461}462463let didReadFromEntangledAsyncAction: boolean = false;464465// Each call to processUpdateQueue should be accompanied by a call to this. It's466// only in a separate function because in updateHostRoot, it must happen after467// all the context stacks have been pushed to, to prevent a stack mismatch. A468// bit unfortunate.469export function suspendIfUpdateReadFromEntangledAsyncAction() {470  // Check if this update is part of a pending async action. If so, we'll471  // need to suspend until the action has finished, so that it's batched472  // together with future updates in the same action.473  // TODO: Once we support hooks inside useMemo (or an equivalent474  // memoization boundary like Forget), hoist this logic so that it only475  // suspends if the memo boundary produces a new value.476  if (didReadFromEntangledAsyncAction) {477    const entangledActionThenable = peekEntangledActionThenable();478    if (entangledActionThenable !== null) {479      // TODO: Instead of the throwing the thenable directly, throw a480      // special object like `use` does so we can detect if it's captured481      // by userspace.482      throw entangledActionThenable;483    }484  }485}486487export function processUpdateQueue<State>(488  workInProgress: Fiber,489  props: any,490  instance: any,491  renderLanes: Lanes,492): void {493  didReadFromEntangledAsyncAction = false;494495  // This is always non-null on a ClassComponent or HostRoot496  const queue: UpdateQueue<State> = workInProgress.updateQueue as any;497498  hasForceUpdate = false;499500  if (__DEV__) {501    currentlyProcessingQueue = queue.shared;502  }503504  let firstBaseUpdate = queue.firstBaseUpdate;505  let lastBaseUpdate = queue.lastBaseUpdate;506507  // Check if there are pending updates. If so, transfer them to the base queue.508  let pendingQueue = queue.shared.pending;509  if (pendingQueue !== null) {510    queue.shared.pending = null;511512    // The pending queue is circular. Disconnect the pointer between first513    // and last so that it's non-circular.514    const lastPendingUpdate = pendingQueue;515    const firstPendingUpdate = lastPendingUpdate.next;516    lastPendingUpdate.next = null;517    // Append pending updates to base queue518    if (lastBaseUpdate === null) {519      firstBaseUpdate = firstPendingUpdate;520    } else {521      lastBaseUpdate.next = firstPendingUpdate;522    }523    lastBaseUpdate = lastPendingUpdate;524525    // If there's a current queue, and it's different from the base queue, then526    // we need to transfer the updates to that queue, too. Because the base527    // queue is a singly-linked list with no cycles, we can append to both528    // lists and take advantage of structural sharing.529    // TODO: Pass `current` as argument530    const current = workInProgress.alternate;531    if (current !== null) {532      // This is always non-null on a ClassComponent or HostRoot533      const currentQueue: UpdateQueue<State> = current.updateQueue as any;534      const currentLastBaseUpdate = currentQueue.lastBaseUpdate;535      if (currentLastBaseUpdate !== lastBaseUpdate) {536        if (currentLastBaseUpdate === null) {537          currentQueue.firstBaseUpdate = firstPendingUpdate;538        } else {539          currentLastBaseUpdate.next = firstPendingUpdate;540        }541        currentQueue.lastBaseUpdate = lastPendingUpdate;542      }543    }544  }545546  // These values may change as we process the queue.547  if (firstBaseUpdate !== null) {548    // Iterate through the list of updates to compute the result.549    let newState = queue.baseState;550    // TODO: Don't need to accumulate this. Instead, we can remove renderLanes551    // from the original lanes.552    let newLanes: Lanes = NoLanes;553554    let newBaseState = null;555    let newFirstBaseUpdate = null;556    let newLastBaseUpdate: null | Update<State> = null;557558    let update: Update<State> = firstBaseUpdate;559    do {560      // An extra OffscreenLane bit is added to updates that were made to561      // a hidden tree, so that we can distinguish them from updates that were562      // already there when the tree was hidden.563      const updateLane = removeLanes(update.lane, OffscreenLane);564      const isHiddenUpdate = updateLane !== update.lane;565566      // Check if this update was made while the tree was hidden. If so, then567      // it's not a "base" update and we should disregard the extra base lanes568      // that were added to renderLanes when we entered the Offscreen tree.569      const shouldSkipUpdate = isHiddenUpdate570        ? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)571        : !isSubsetOfLanes(renderLanes, updateLane);572573      if (shouldSkipUpdate) {574        // Priority is insufficient. Skip this update. If this is the first575        // skipped update, the previous update/state is the new base576        // update/state.577        const clone: Update<State> = {578          lane: updateLane,579580          tag: update.tag,581          payload: update.payload,582          callback: update.callback,583584          next: null,585        };586        if (newLastBaseUpdate === null) {587          newFirstBaseUpdate = newLastBaseUpdate = clone;588          newBaseState = newState;589        } else {590          newLastBaseUpdate = newLastBaseUpdate.next = clone;591        }592        // Update the remaining priority in the queue.593        newLanes = mergeLanes(newLanes, updateLane);594      } else {595        // This update does have sufficient priority.596597        // Check if this update is part of a pending async action. If so,598        // we'll need to suspend until the action has finished, so that it's599        // batched together with future updates in the same action.600        if (updateLane !== NoLane && updateLane === peekEntangledActionLane()) {601          didReadFromEntangledAsyncAction = true;602        }603604        if (newLastBaseUpdate !== null) {605          const clone: Update<State> = {606            // This update is going to be committed so we never want uncommit607            // it. Using NoLane works because 0 is a subset of all bitmasks, so608            // this will never be skipped by the check above.609            lane: NoLane,610611            tag: update.tag,612            payload: update.payload,613614            // When this update is rebased, we should not fire its615            // callback again.616            callback: null,617618            next: null,619          };620          newLastBaseUpdate = newLastBaseUpdate.next = clone;621        }622623        // Process this update.624        newState = getStateFromUpdate(625          workInProgress,626          queue,627          update,628          newState,629          props,630          instance,631        );632        const callback = update.callback;633        if (callback !== null) {634          workInProgress.flags |= Callback;635          if (isHiddenUpdate) {636            workInProgress.flags |= Visibility;637          }638          const callbacks = queue.callbacks;639          if (callbacks === null) {640            queue.callbacks = [callback];641          } else {642            callbacks.push(callback);643          }644        }645      }646      // $FlowFixMe[incompatible-type] we bail out when we get a null647      update = update.next;648      if (update === null) {649        pendingQueue = queue.shared.pending;650        if (pendingQueue === null) {651          break;652        } else {653          // An update was scheduled from inside a reducer. Add the new654          // pending updates to the end of the list and keep processing.655          const lastPendingUpdate = pendingQueue;656          // Intentionally unsound. Pending updates form a circular list, but we657          // unravel them when transferring them to the base queue.658          const firstPendingUpdate =659            lastPendingUpdate.next as any as Update<State>;660          lastPendingUpdate.next = null;661          update = firstPendingUpdate;662          queue.lastBaseUpdate = lastPendingUpdate;663          queue.shared.pending = null;664        }665      }666    } while (true);667668    if (newLastBaseUpdate === null) {669      newBaseState = newState;670    }671672    queue.baseState = newBaseState as any as State;673    queue.firstBaseUpdate = newFirstBaseUpdate;674    queue.lastBaseUpdate = newLastBaseUpdate;675676    // $FlowFixMe[invalid-compare]677    if (firstBaseUpdate === null) {678      // `queue.lanes` is used for entangling transitions. We can set it back to679      // zero once the queue is empty.680      queue.shared.lanes = NoLanes;681    }682683    // Set the remaining expiration time to be whatever is remaining in the queue.684    // This should be fine because the only two other things that contribute to685    // expiration time are props and context. We're already in the middle of the686    // begin phase by the time we start processing the queue, so we've already687    // dealt with the props. Context in components that specify688    // shouldComponentUpdate is tricky; but we'll have to account for689    // that regardless.690    markSkippedUpdateLanes(newLanes);691    workInProgress.lanes = newLanes;692    workInProgress.memoizedState = newState;693  }694695  if (__DEV__) {696    currentlyProcessingQueue = null;697  }698}699700function callCallback(callback: () => mixed, context: any) {701  if (typeof callback !== 'function') {702    throw new Error(703      'Invalid argument passed as callback. Expected a function. Instead ' +704        `received: ${callback}`,705    );706  }707708  callback.call(context);709}710711export function resetHasForceUpdateBeforeProcessing() {712  hasForceUpdate = false;713}714715export function checkHasForceUpdateAfterProcessing(): boolean {716  return hasForceUpdate;717}718719export function deferHiddenCallbacks<State>(720  updateQueue: UpdateQueue<State>,721): void {722  // When an update finishes on a hidden component, its callback should not723  // be fired until/unless the component is made visible again. Stash the724  // callback on the shared queue object so it can be fired later.725  const newHiddenCallbacks = updateQueue.callbacks;726  if (newHiddenCallbacks !== null) {727    const existingHiddenCallbacks = updateQueue.shared.hiddenCallbacks;728    if (existingHiddenCallbacks === null) {729      updateQueue.shared.hiddenCallbacks = newHiddenCallbacks;730    } else {731      updateQueue.shared.hiddenCallbacks =732        existingHiddenCallbacks.concat(newHiddenCallbacks);733    }734  }735}736737export function commitHiddenCallbacks<State>(738  updateQueue: UpdateQueue<State>,739  context: any,740): void {741  // This component is switching from hidden -> visible. Commit any callbacks742  // that were previously deferred.743  const hiddenCallbacks = updateQueue.shared.hiddenCallbacks;744  if (hiddenCallbacks !== null) {745    updateQueue.shared.hiddenCallbacks = null;746    for (let i = 0; i < hiddenCallbacks.length; i++) {747      const callback = hiddenCallbacks[i];748      callCallback(callback, context);749    }750  }751}752753export function commitCallbacks<State>(754  updateQueue: UpdateQueue<State>,755  context: any,756): void {757  const callbacks = updateQueue.callbacks;758  if (callbacks !== null) {759    updateQueue.callbacks = null;760    for (let i = 0; i < callbacks.length; i++) {761      const callback = callbacks[i];762      callCallback(callback, context);763    }764  }765}

Findings

✓ No findings reported for this file.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.