The Data Structure behind React —Part 1 — createRoot()
Its Fiber
Preface
When we start with React, below is the file that is created for each project!
import React, { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App';
const rootElement = document.getElementById('root');
const root = createRoot(rootElement);
root.render(
<StrictMode>
<App />
</StrictMode>
);
The first 3 lines are import statements. The next line is getting the root element, then we do createRoot.
We use the value returned by createRoot and render the App. Usually all our components are rendered with App, or we might use some kind of an Router Outlet and wrap the App with Routers and other Contexts!
But what does React do internally when we do createRoot and root.render? Let’s find out!
createRoot
From the docs:createRoot
lets you create a root to display React components inside a browser DOM node.
React will create a root for the domNode
, and take over managing the DOM inside it. After you’ve created a root, you need to call root.render
to display a React component inside of it.
TLDR: Flow Diagram
As we can see in the Flow Diagram, when we call createRoot(element) -> It checks whether the element is a valid container not. If its is valid it sets the options then calls createContainer.
createContainer returns FiberRoot, then markContainerAsRoot is called, then we pass the rootContainedElement to listen to supported events.
Finally we return new ReactDOMRoot(root), using this returned value, we call root.render() to render the React Components.
While this is the overall flow of createRoot method, let’s see what each method does!
isValidContainer
isValidContainer takes in the element that we pass and checks whether the element is non-null and if it is of HTMLNodeType. If both are true it returns true else false.
createContainer
This is the main function where actual container is created i.e the Fiber Node.
Below is the createContainer signature, in this article we will focus on containerInfo and tag.
function createContainer(
containerInfo: Container,
tag: RootTag,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
isStrictMode: boolean,
// TODO: Remove `concurrentUpdatesByDefaultOverride`. It is now ignored.
concurrentUpdatesByDefaultOverride: null | boolean,
identifierPrefix: string,
onUncaughtError: (
error: mixed,
errorInfo: {+componentStack?: ?string},
) => void,
onCaughtError: (
error: mixed,
errorInfo: {
+componentStack?: ?string,
+errorBoundary?: ?React$Component<any, any>,
},
) => void,
onRecoverableError: (
error: mixed,
errorInfo: {+componentStack?: ?string},
) => void,
transitionCallbacks: null | TransitionTracingCallbacks,
): OpaqueRoot
containerInfo is the element that we have passed to createRoot.
What is tag though → tag is of type RootTag, RootTag can either be LegacyRoot (0) or ConcurrentRoot (1).
ConcurrentRoot is the tag that is passed with React 18 now.
Now let’s see flow diagram of createContainer
createContainer internally calls createFiberRoot.
createFiberRoot creates a new FiberRoot Node using container and tag.
A quick reminder :) container is the element that we passed to createRoot and tag is ConcurrentRoot. Below is the type of FiberRoot.
In upcoming parts we will we will understand what each of them entails, but for now we will only focus on following properties → containerInfo, tag, and current (BaseFiberRootProperties).
type FiberRoot = {
...BaseFiberRootProperties,
...SuspenseCallbackOnlyFiberRootProperties,
...UpdaterTrackingOnlyFiberRootProperties,
...TransitionTracingOnlyFiberRootProperties,
};
type BaseFiberRootProperties = {
tag: RootTag,
containerInfo: Container,
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,
finishedWork: Fiber | null,
timeoutHandle: TimeoutHandle | NoTimeout,
cancelPendingCommit: null | (() => void),
context: Object | null,
pendingContext: Object | null,
next: FiberRoot | null,
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,
identifierPrefix: string,
onUncaughtError: (
error: mixed,
errorInfo: {+componentStack?: ?string},
) => void,
onCaughtError: (
error: mixed,
errorInfo: {
+componentStack?: ?string,
+errorBoundary?: ?React$Component<any, any>,
},
) => void,
onRecoverableError: (
error: mixed,
errorInfo: {+componentStack?: ?string},
) => void,
formState: ReactFormState<any, any> | null,
};
Once the root node is created, createHostRootFiber is called with tag and isStrictMode.
createHostRootFiber is the function that creates Fiber for the root container. It internally calls createFiber method with a WorkTag and a Mode.
WorkTag is different than RootTag. You might remember that we had ConcurrentRoot tag when creating the root container.
To identify the type of Fiber, we have tag property of type: WorkTag.
WorkTag can be any of the following things
export const FunctionComponent = 0;
export const ClassComponent = 1;
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;
export const IncompleteFunctionComponent = 28;
export const Throw = 29;
As we creating Fiber for Host Root, the work tag here will be HostRoot
Below are the properties that a Fiber contains.
// 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 = {
// 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,
return: Fiber | null,
// Singly Linked List Tree Structure.
child: Fiber | null,
sibling: Fiber | null,
index: number,
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,
mode: TypeOfMode,
// Effect
flags: Flags,
subtreeFlags: Flags,
deletions: Array<Fiber> | null,
lanes: Lanes,
childLanes: Lanes,
alternate: Fiber | null,
actualDuration?: number,
actualStartTime?: number,
selfBaseDuration?: number,
treeBaseDuration?: number,
_debugInfo?: ReactDebugInfo | null,
_debugOwner?: ReactComponentInfo | Fiber | null,
_debugStack?: string | Error | null,
_debugTask?: ConsoleTask | null,
_debugIsCurrentlyTiming?: boolean,
_debugNeedsRemount?: boolean,
_debugHookTypes?: Array<HookType> | null,
};
So what is a Fiber! Well from Github Comments, we have
A Fiber is work on a Component that needs to be done or was done. There can be more than one per component.
Each Fiber node contains all the details of its Component (its state, props) and is used to manage all the state updates of its component!
In upcoming parts we will see how different properties of Fiber is used to render Component and do state updates!
Post Fiber creation, Fiber is attached to rootContainer like below
const uninitializedFiber = createHostRootFiber(tag, isStrictMode);
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
const initialState: RootState = {
element: initialChildren,
...otherProps
};
uninitializedFiber.memoizedState = initialState;
initializeUpdateQueue(uninitializedFiber);
UpdateQueue of a Fiber is used to manage the state updates or side effects that are to be performed in the component.
initializeUpdateQueue is a simple method to set the update queue in the fiber node with its baseState as fiber.memoizedState
createContainer method is done now!
markContainerAsRoot
Now we mark the container as root. It takes in the Fiber node and the element that we passed to createRoot method.
markContainerAsRoot(root.current, container);
// Implementation
export function markContainerAsRoot(hostRoot: Fiber, node: Container): void {
node[internalContainerInstanceKey] = hostRoot;
}
// internalContainerInstanceKey is a random unique key generated
listenToAllSupportedEvents
Now that we have the container ready, we pass the container to listenToAllSupportedEvents to register for all the events.
listenToAllSupportedEvents internally registers for all native events.
It goes through each native event and creates the listener using the method → createEventListenerWrapperWithPriority which is part of ReactDOMEventListener.
Finally the listeners are registered in both capturing phase and bubbling phase using addEventCaptureListener and addEventBubbleListener method which are part of EventListener.
Finally we return return new ReactDOMRoot(root)
Using this return value we do
root.render(
<StrictMode>
<App />
</StrictMode>
);
You must be wondering, where does the render method is defined!
Well render method is part of ReactDOMRoot’s prototype.
It accepts children and internally calls updateContainer method.
So createRoot calls createContainer and render calls updateContainer!
Thanks
In Plain English 🚀
Thank you for being a part of the In Plain English community! Before you go:
- Be sure to clap and follow the writer ️👏️️
- Follow us: X | LinkedIn | YouTube | Discord | Newsletter
- Visit our other platforms: CoFeed | Differ
- More content at PlainEnglish.io