Data Structure in React
In this part we will have a glance through the core components that affect the data structure in React, including FiberRoot
, Fiber
, effectTags
, ReactWorkTag
, Update
and UpdateQueue
and compare both the latest version and the version on v16. More details will be discussed in later section.
FiberRoot
V18
type BaseFiberRootProperties = {
// The type of root (legacy, batched, concurrent, etc.)
tag: RootTag,
// Any additional information from the host associated with this root.
containerInfo: Container,
// Used only by persistent updates.
pendingChildren: any,
// The currently active root fiber. This is the mutable root of the tree.
current: Fiber,
pingCache: WeakMap<Wakeable, Set<mixed>> | Map<Wakeable, Set<mixed>> | null,
// A finished work-in-progress HostRoot that's ready to be committed.
finishedWork: Fiber | null,
// Timeout handle returned by setTimeout. Used to cancel a pending timeout, if
// it's superseded by a new one.
timeoutHandle: TimeoutHandle | NoTimeout,
// When a root has a pending commit scheduled, calling this function will
// cancel it.
// TODO: Can this be consolidated with timeoutHandle?
cancelPendingCommit: null | (() => void),
// Top context object, used by renderSubtreeIntoContainer
context: Object | null,
pendingContext: Object | null,
// Used to create a linked list that represent all the roots that have
// pending work scheduled on them.
next: FiberRoot | null,
// Node returned by Scheduler.scheduleCallback. Represents the next rendering
// task that the root will work on.
callbackNode: any,
callbackPriority: Lane,
expirationTimes: LaneMap<number>,
hiddenUpdates: LaneMap<Array<ConcurrentUpdate> | null>,
pendingLanes: Lanes,
suspendedLanes: Lanes,
pingedLanes: Lanes,
expiredLanes: Lanes,
errorRecoveryDisabledLanes: Lanes,
shellSuspendCounter: number,
finishedLanes: Lanes,
entangledLanes: Lanes,
entanglements: LaneMap<Lanes>,
pooledCache: Cache | null,
pooledCacheLanes: Lanes,
// TODO: In Fizz, id generation is specific to each server config. Maybe we
// should do this in Fiber, too? Deferring this decision for now because
// there's no other place to store the prefix except for an internal field on
// the public createRoot object, which the fiber tree does not currently have
// a reference to.
identifierPrefix: string,
onRecoverableError: (
error: mixed,
errorInfo: {digest?: ?string, componentStack?: ?string},
) => void,
formState: ReactFormState<any, any> | null,
};
V16
type BaseFiberRootProperties = {|
// it's a node in root, second argument of render
containerInfo: any,
// only being used in persisted updates( aka doesn't support incremental updates), neither used in react-dom
pendingChildren: any,
// current target fiber, the root of the tree
current: Fiber,
// Below code are used for separating priorities of tasks
// 1) Task that are not commited
// 2) Suspence that are not committed
// 3) Suspence that might be committed later
// To improve performance, it chose to not trace every single blocking registration
// The earliest and latest priority levels that are suspended from committing.
// The earlist and latest tasks that are await for rendering
earliestSuspendedTime: ExpirationTime,
latestSuspendedTime: ExpirationTime,
// The earliest and latest priority levels that are not known to be suspended.
// The earliest and latest tasks that are pending/unsure of the render(The initial state of all the tasks when they come in)
earliestPendingTime: ExpirationTime,
latestPendingTime: ExpirationTime,
// The latest priority level that was pinged by a resolved promise and can
// be retried.
// The least priority that can be resolved via a promise
latestPingedTime: ExpirationTime,
// If there are error being thrown, and there are no further updates, it will try to rendering from the start before handling the error
// if there are errors that can't be handled at `renderRoot`, this `didError` will be marked as `true`
didError: boolean,
// `expirationTime` of the task that are await to be committed
pendingCommitExpirationTime: ExpirationTime,
// The FiberRoot target that already finished rendering, if you only have one root, then it will always be the fiber in this Root, or it is null
// During commit phase, React will only handle the task related to `finishedWord`
finishedWork: Fiber | null,
// The returned result when the tasks are being handled by `setTimeout`, when there is a next new task it will trigger the timeout yet to be cleaned up
timeoutHandle: TimeoutHandle | NoTimeout,
// Top level context, it will only be used when `renderSubtreeIntoContainer`
context: Object | null,
pendingContext: Object | null,
// Used to make sure if the first render needs hydration
+hydrate: boolean,
// The rest of expirationTime in current root
// TODO: explain how the renderer separate the target to handle
nextExpirationTimeToWorkOn: ExpirationTime,
// The current expirationTime's expirationTime
expirationTime: ExpirationTime,
// List of top-level batches. This list indicates whether a commit should be
// deferred. Also contains completion callbacks.
// TODO: Lift this into the renderer
firstBatch: Batch | null,
// How the Roots are related -> to handle the next root
nextScheduledRoot: FiberRoot | null,
|};
Changes
- adding
tag
to specify the type of root - Improve performance: separate suspended time are removed and handled better by
Lane
,expirationTimes
andcallbackPriority
- Better error handling: remove
didError
as boolean, and make themonRecoverable Error
with specific error information
Reference: ReactInternalTypes.ts (opens in a new tab)
Fiber
As seen in reference of ReactInternalTypes.ts (opens in a new tab)
V18
// A Fiber is work on a Component that needs to be done or was done. There can
// be more than one per component.
export type Fiber = {
// These first fields are conceptually members of an Instance. This used to
// be split into a separate type and intersected with the other Fiber fields,
// but until Flow fixes its intersection bugs, we've merged them into a
// single type.
// An Instance is shared between all versions of a component. We can easily
// break this out into a separate object to avoid copying so much to the
// alternate versions of the tree. We put this on a single object for now to
// minimize the number of objects created during the initial render.
// Tag identifying the type of fiber.
tag: WorkTag,
// Unique identifier of this child.
key: null | string,
// The value of element.type which is used to preserve the identity during
// reconciliation of this child.
elementType: any,
// The resolved function/class/ associated with this fiber.
type: any,
// The local state associated with this fiber.
stateNode: any,
// Conceptual aliases
// parent : Instance -> return The parent happens to be the same as the
// return fiber since we've merged the fiber and instance.
// Remaining fields belong to Fiber
// The Fiber to return to after finishing processing this one.
// This is effectively the parent, but there can be multiple parents (two)
// so this is only the parent of the thing we're currently processing.
// It is conceptually the same as the return address of a stack frame.
return: Fiber | null,
// Singly Linked List Tree Structure.
child: Fiber | null,
sibling: Fiber | null,
index: number,
// The ref last used to attach this node.
// I'll avoid adding an owner field for prod and model that as functions.
ref:
| null
| (((handle: mixed) => void) & {_stringRef: ?string, ...})
| RefObject,
refCleanup: null | (() => void),
// Input is the data coming into process this fiber. Arguments. Props.
pendingProps: any, // This type will be more specific once we overload the tag.
memoizedProps: any, // The props used to create the output.
// A queue of state updates and callbacks.
updateQueue: mixed,
// The state used to create the output
memoizedState: any,
// Dependencies (contexts, events) for this fiber, if it has any
dependencies: Dependencies | null,
// Bitfield that describes properties about the fiber and its subtree. E.g.
// the ConcurrentMode flag indicates whether the subtree should be async-by-
// default. When a fiber is created, it inherits the mode of its
// parent. Additional flags can be set at creation time, but after that the
// value should remain unchanged throughout the fiber's lifetime, particularly
// before its child fibers are created.
mode: TypeOfMode,
// Effect
flags: Flags,
subtreeFlags: Flags,
deletions: Array<Fiber> | null,
// Singly linked list fast path to the next fiber with side-effects.
nextEffect: Fiber | null,
// The first and last fiber with side-effect within this subtree. This allows
// us to reuse a slice of the linked list when we reuse the work done within
// this fiber.
firstEffect: Fiber | null,
lastEffect: Fiber | null,
lanes: Lanes,
childLanes: Lanes,
// This is a pooled version of a Fiber. Every fiber that gets updated will
// eventually have a pair. There are cases when we can clean up pairs to save
// memory if we need to.
alternate: Fiber | null,
// Time spent rendering this Fiber and its descendants for the current update.
// This tells us how well the tree makes use of sCU for memoization.
// It is reset to 0 each time we render and only updated when we don't bailout.
// This field is only set when the enableProfilerTimer flag is enabled.
actualDuration?: number,
// If the Fiber is currently active in the "render" phase,
// This marks the time at which the work began.
// This field is only set when the enableProfilerTimer flag is enabled.
actualStartTime?: number,
// Duration of the most recent render time for this Fiber.
// This value is not updated when we bailout for memoization purposes.
// This field is only set when the enableProfilerTimer flag is enabled.
selfBaseDuration?: number,
// Sum of base times for all descendants of this Fiber.
// This value bubbles up during the "complete" phase.
// This field is only set when the enableProfilerTimer flag is enabled.
treeBaseDuration?: number,
// Conceptual aliases
// workInProgress : Fiber -> alternate The alternate used for reuse happens
// to be the same as work in progress.
// __DEV__ only
_debugOwner?: Fiber | null,
_debugIsCurrentlyTiming?: boolean,
_debugNeedsRemount?: boolean,
// Used to verify that the order of hooks does not change between renders.
_debugHookTypes?: Array<HookType> | null,
};
V16
type Fiber = {|
// Tag identifying the type of fiber.
tag: WorkTag,
// Unique identifier of this child.
key: null | string,
// The value of element.type which is used to preserve the identity during
// reconciliation of this child.
elementType: any,
// The resolved function/class/ associated with this fiber. These are usually functions or classes, but type is kept generic to avoid recasting for flow.
type: any,
// The local state associated with this fiber.
stateNode: any,
// Pointing towards the `parent` fiber, which is the same as the `return` fiber since we've merged the fiber and instance.
return: Fiber | null,
// Singly Linked List Tree Structure.
child: Fiber | null,
sibling: Fiber | null,
index: number,
// The attribute of the ref last used to attach this node.
ref:
| null
| (((handle: mixed) => void) & {_stringRef: ?string, ...})
| RefObject,
// new props brought up by changes
pendingProps: any, // This type will be more specific once we overload the tag.
memoizedProps: any, // The props from the last render.
// Updates of components in this fiber will be stored in this queue
updateQueue: UpdateQueue<any> | null,
// The state from last render
memoizedState: any,
// A list to store the depending context of this Fiber
firstContextDependency: ContextDependency<mixed> | null,
// Used to describe current Fiber and the children's `Bitfield`. The shared mode means the childrens are not rendered separately, When the Fiber is created, it will inherit the mode of its parent. Additional flags can be set at creation time, but after that the value should remain unchanged throughout the fiber's lifetime, particularly before its child fibers are created.
mode: TypeOfMode,
// Effect
// Used to describe Side Effects of the Fiber
effectTags: SideEffectTag,
// Single linked list is used to search for the next side effect
nextEffect: Fiber | null,
// The first side effect in the children's tree
firstEffect: Fiber | null,
// The last side effect in the children's tree
lastEffect: Fiber | null,
// Represents the expirationTime of the Fiber render, but doesn't include its children's expirationTime
expirationTime: ExpirationTime,
// To confirm whether there is pending changes in the children's tree
childExpirationTime: ExpirationTime,
// When the Fiber tree is pending for an update, each Fiber will have a corresponding Fiber
// We call them `current` <==> `workInProgress` and they will swap position after rendering
alternate: Fiber | null,
// Below content are used for configuration/collection of each Fiber and their children's render time, also used for debugging
actualDuration?: number,
// If the Fiber is currently active in the "render" phase,
// This marks the time at which the work began.
// This field is only set when the enableProfilerTimer flag is enabled.
actualStartTime?: number,
// Duration of the most recent render time for this Fiber.
// This value is not updated when we bailout for memoization purposes.
// This field is only set when the enableProfilerTimer flag is enabled.
selfBaseDuration?: number,
// Sum of base times for all descedents of this Fiber.
// This value bubbles up during the "complete" phase.
// This field is only set when the enableProfilerTimer flag is enabled.
treeBaseDuration?: number,
// Conceptual aliases
// workInProgress : Fiber -> alternate The alternate used for reuse happens
// to be the same as work in progress.
// __DEV__ only
_debugID?: number,
_debugSource?: Source | null,
_debugOwner?: Fiber | null,
_debugIsCurrentlyTiming?: boolean,
|};
Changes
Clean up
- Specifically cleaning up after
ref
. If clean up function is not provided,ref
function is called with null (opens in a new tab) like it has been before. - Remove
_debugID
(opens in a new tab) and_debugSource
fields as they are not really needed and adds costs but adds_debugHookTypes
for hooks and a boolean_debugNeedsRemount
for quick debug status confirmation.
Clearer naming and separations
firstContextDependency
is nowdependencies
effectTags
is now separated intoflags
,subtreeFlags
anddeletions
. See Effects list refactor (opens in a new tab) and combine flags and subtreeFlags (opens in a new tab) whilst the effect algorithm is unchanged.expirationTime
is nowlanes
andchildExpirationTime
is nowchildLanes
. Consequently Andrew simplifies them by splitting the types (opens in a new tab) and then make expirationTime an opaque type (opens in a new tab) in order to replace previous scheduler work in reconciler, as a more apparent way of how tasks are being queued.
effectTags -> FiberFlags
V18
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import {enableCreateEventHandleAPI} from 'shared/ReactFeatureFlags';
export type Flags = number;
// Don't change these values. They're used by React Dev Tools.
export const NoFlags = /* */ 0b0000000000000000000000000000;
export const PerformedWork = /* */ 0b0000000000000000000000000001;
export const Placement = /* */ 0b0000000000000000000000000010;
export const DidCapture = /* */ 0b0000000000000000000010000000;
export const Hydrating = /* */ 0b0000000000000001000000000000;
// You can change the rest (and add more).
export const Update = /* */ 0b0000000000000000000000000100;
/* Skipped value: 0b0000000000000000000000001000; */
export const ChildDeletion = /* */ 0b0000000000000000000000010000;
export const ContentReset = /* */ 0b0000000000000000000000100000;
export const Callback = /* */ 0b0000000000000000000001000000;
/* Used by DidCapture: 0b0000000000000000000010000000; */
export const ForceClientRender = /* */ 0b0000000000000000000100000000;
export const Ref = /* */ 0b0000000000000000001000000000;
export const Snapshot = /* */ 0b0000000000000000010000000000;
export const Passive = /* */ 0b0000000000000000100000000000;
/* Used by Hydrating: 0b0000000000000001000000000000; */
export const Visibility = /* */ 0b0000000000000010000000000000;
export const StoreConsistency = /* */ 0b0000000000000100000000000000;
// It's OK to reuse these bits because these flags are mutually exclusive for
// different fiber types. We should really be doing this for as many flags as
// possible, because we're about to run out of bits.
export const ScheduleRetry = StoreConsistency;
export const ShouldSuspendCommit = Visibility;
export const DidDefer = ContentReset;
export const LifecycleEffectMask =
Passive | Update | Callback | Ref | Snapshot | StoreConsistency;
// Union of all commit flags (flags with the lifetime of a particular commit)
export const HostEffectMask = /* */ 0b0000000000000111111111111111;
// These are not really side effects, but we still reuse this field.
export const Incomplete = /* */ 0b0000000000001000000000000000;
export const ShouldCapture = /* */ 0b0000000000010000000000000000;
export const ForceUpdateForLegacySuspense = /* */ 0b0000000000100000000000000000;
export const DidPropagateContext = /* */ 0b0000000001000000000000000000;
export const NeedsPropagation = /* */ 0b0000000010000000000000000000;
export const Forked = /* */ 0b0000000100000000000000000000;
// Static tags describe aspects of a fiber that are not specific to a render,
// e.g. a fiber uses a passive effect (even if there are no updates on this particular render).
// This enables us to defer more work in the unmount case,
// since we can defer traversing the tree during layout to look for Passive effects,
// and instead rely on the static flag as a signal that there may be cleanup work.
export const RefStatic = /* */ 0b0000001000000000000000000000;
export const LayoutStatic = /* */ 0b0000010000000000000000000000;
export const PassiveStatic = /* */ 0b0000100000000000000000000000;
export const MaySuspendCommit = /* */ 0b0001000000000000000000000000;
// Flag used to identify newly inserted fibers. It isn't reset after commit unlike `Placement`.
export const PlacementDEV = /* */ 0b0010000000000000000000000000;
export const MountLayoutDev = /* */ 0b0100000000000000000000000000;
export const MountPassiveDev = /* */ 0b1000000000000000000000000000;
// Groups of flags that are used in the commit phase to skip over trees that
// don't contain effects, by checking subtreeFlags.
export const BeforeMutationMask: number =
// TODO: Remove Update flag from before mutation phase by re-landing Visibility
// flag logic (see #20043)
Update |
Snapshot |
(enableCreateEventHandleAPI
? // createEventHandle needs to visit deleted and hidden trees to
// fire beforeblur
// TODO: Only need to visit Deletions during BeforeMutation phase if an
// element is focused.
ChildDeletion | Visibility
: 0);
export const MutationMask =
Placement |
Update |
ChildDeletion |
ContentReset |
Ref |
Hydrating |
Visibility;
export const LayoutMask = Update | Callback | Ref | Visibility;
// TODO: Split into PassiveMountMask and PassiveUnmountMask
export const PassiveMask = Passive | Visibility | ChildDeletion;
// Union of tags that don't get reset on clones.
// This allows certain concepts to persist without recalculating them,
// e.g. whether a subtree contains passive effects or portals.
export const StaticMask =
LayoutStatic | PassiveStatic | RefStatic | MaySuspendCommit;
V16
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export type SideEffectTag = number;
// Don't change these two values. They're used by React Dev Tools.
export const NoEffect = /* */ 0b00000000000;
export const PerformedWork = /* */ 0b00000000001;
// You can change the rest (and add more).
export const Placement = /* */ 0b00000000010;
export const Update = /* */ 0b00000000100;
export const PlacementAndUpdate = /* */ 0b00000000110;
export const Deletion = /* */ 0b00000001000;
export const ContentReset = /* */ 0b00000010000;
export const Callback = /* */ 0b00000100000;
export const DidCapture = /* */ 0b00001000000;
export const Ref = /* */ 0b00010000000;
export const Snapshot = /* */ 0b00100000000;
// Update & Callback & Ref & Snapshot
export const LifecycleEffectMask = /* */ 0b00110100100;
// Union of all host effects
export const HostEffectMask = /* */ 0b00111111111;
export const Incomplete = /* */ 0b01000000000;
export const ShouldCapture = /* */ 0b10000000000;
Changes
V18 (opens in a new tab) renamed ReactSideEffectTags
to ReactFiberFlags
(ReactFiberFlags) to broaden fiber types. The contants in the old ReactSideEffectTags (opens in a new tab) used bitwise operations to efficiently track and manage the effects of updates to the virtual DOM + prevent app crashing/bailout. effectTags
and sideEffects
are consolidated into one file FiberFlags
.
And yes, running out of bits in React is a real fucking risk.
ReactWorkTag
V18
export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // Before we know whether it is function or class
export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5;
export const HostText = 6;
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
export const DehydratedFragment = 18;
export const SuspenseListComponent = 19;
export const ScopeComponent = 21;
export const OffscreenComponent = 22;
export const LegacyHiddenComponent = 23;
export const CacheComponent = 24;
export const TracingMarkerComponent = 25;
export const HostHoistable = 26;
export const HostSingleton = 27;
V16
export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // Before we know whether it is function or class
export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5;
export const HostText = 6;
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
Changes
The new version added DehydratedFragment
, SuspenseListComponent
, ScopeComponent
, OffscreenComponent
, LegacyHiddenComponent
, CacheComponent
, TracingMarkerComponent
, HostHoistable
, HostSingleton
to the list of ReactWorkTag
. We will talk about some of them (ex: SuspenseListComponent
) in the later section.
Update & UpdateQueue
V18
export type Update<State> = {
lane: Lane,
tag: 0 | 1 | 2 | 3,
payload: any,
callback: (() => mixed) | null,
next: Update<State> | null,
};
export type SharedQueue<State> = {
pending: Update<State> | null,
lanes: Lanes,
hiddenCallbacks: Array<() => mixed> | null,
};
export type UpdateQueue<State> = {
baseState: State,
firstBaseUpdate: Update<State> | null,
lastBaseUpdate: Update<State> | null,
shared: SharedQueue<State>,
callbacks: Array<() => mixed> | null,
};
export const UpdateState = 0;
export const ReplaceState = 1;
export const ForceUpdate = 2;
export const CaptureUpdate = 3;
V16
export type Update<State> = {
// The updated expiration time
expirationTime: ExpirationTime,
// export const UpdateState = 0;
// export const ReplaceState = 1;
// export const ForceUpdate = 2;
// export const CaptureUpdate = 3;
// Four states of updates with their own tag numbers
tag: 0 | 1 | 2 | 3,
// Updated content. ex: the first argument of `setState`
payload: any,
// Callback function. ex: the second argument of `setState`
callback: (() => mixed) | null,
// The next update in the queue
next: Update<State> | null,
// The priority level of the update, next `side effect`
nextEffect: Update<State> | null,
};
export type UpdateQueue<State> = {
// The state after each update
baseState: State,
// The first update in the queue
firstUpdate: Update<State> | null,
// The last update in the queue
lastUpdate: Update<State> | null,
// The first captured update in the queue
firstCapturedUpdate: Update<State> | null,
// The last captured update in the queue
lastCapturedUpdate: Update<State> | null,
// The first `side effect`
firstEffect: Update<State> | null,
// The last `side effect`
lastEffect: Update<State> | null,
// The first captured `side effect` and the last
firstCapturedEffect: Update<State> | null,
lastCapturedEffect: Update<State> | null,
};
Changes
- Both
Update
andUpdateQueue
are simplified inV18
. AddedSharedQueue
. expirationTime
is removed fromUpdate
and replaced bylane
inV18
according to the newLane
expression.- Removed all redundant UpdateStates and consolidated them into
lane
/pending
.