Skip to content

Instantly share code, notes, and snippets.

@hi-ogawa
Last active August 29, 2023 07:49
Show Gist options
  • Save hi-ogawa/9b74de8b1ea9d739088ab6c52096004b to your computer and use it in GitHub Desktop.
Save hi-ogawa/9b74de8b1ea9d739088ab6c52096004b to your computer and use it in GitHub Desktop.
Reading React

todo / summary

  • dev setup (debugger, testing, ...)
  • build system and package organization
    • patching ReactFiberHostConfig and ReactSharedLibrary (cf. scripts/shared/inlinedHostConfigs.js)
    • rollup, babel, jest configurations
  • hook and rendering lifecycle
    • mount
    • update
    • unmount
  • hot reload
  • dom integration (ReactDOMHostConfig)
  • suspense (cf. ReactFiberThrow)
  • ref
    • callback phase (around layout effect?)
    • forward
  • reconcilation algorithm (reconcileChildFibers)

references

misc

# install dependencies
yarn

# for vscode extension to discover .flowconfig
ln -sfr scripts/flow/custom/.flowconfig .flowconfig

# build for minimal demo page
yarn build react/index react-dom/index --type=UMD_DEV
xdg-open fixtures/packaging/babel-standalone/dev.html

# testing
yarn test packages/react-test-renderer/src/__tests__/ReactTestRendererAct-test.js

# debugging (cf. https://jestjs.io/docs/troubleshooting#debugging-in-vs-code)
# See below for how to enable source map. (this issue is also relevant https://github.com/facebook/react/issues/14361)
yarn test --debug packages/react-test-renderer/src/__tests__/ReactTestRendererAct-test.js # then hit F5
yarn test --debug packages/react-reconciler/src/__tests__/ReactEffectOrdering-test.js -t 'passive unmounts on deletion are fired'
  • .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Attach",
      "type": "node",
      "request": "attach",
      "port": 9229
    }
  ]
}
  • patch to enable source map in jest tests
diff --git a/scripts/jest/preprocessor.js b/scripts/jest/preprocessor.js
index d7a5a2cda..a27ffd09d 100644
--- a/scripts/jest/preprocessor.js
+++ b/scripts/jest/preprocessor.js
@@ -50,6 +50,7 @@ const babelOptions = {
     ],
   ],
   retainLines: true,
+  sourceMaps: "inline",
 };

 module.exports = {

test-renderer and reconciler

  • example
// react/packages/react-test-renderer/src/__tests__/ReactTestRendererAct-test.js
function App(props) {
  const [ctr, setCtr] = React.useState(0);
  React.useEffect(() => {
    props.callback();
    setCtr(1);
  }, []);
  return ctr;
}
const calledLog = [];
let root;
act(() => {
  root = ReactTestRenderer.create(
    <App
      callback={() => {
        calledLog.push(calledLog.length);
      }}
    />
  );
});
  • top level
act =>
  initialize ReactCurrentActQueue.current to skip scheduler logic for testing
  (callback) =>
    React.createElement(App, ...) (instantiate `ReactElement` with `App` type)
    ReactTestRenderer.create =>
      createContainer (returns FiberRoot) =>
        createFiberRoot =>
          new FiberRootNode (create FiberRoot)
          createHostRootFiber (createFiber with `HostRoot` tag)
          initializeUpdateQueue
      updateContainer(ReactNodeList, FiberRoot) =>
        requestUpdateLane
        createUpdate (`UpdateState` tag with `ReactNodeList` payload (here `<App />` element))
        enqueueUpdate (insert `Update` to `Fiber.updateQueue` linked list)
        scheduleUpdateOnFiber =>
          ensureRootIsScheduled =>
            scheduleLegacySyncCallback(performSyncWorkOnRoot)
            scheduleCallback(..., flushSyncCallbacks) (scheduleMicrotask if available) =>
              push to ReactCurrentActQueue
  flushActQueue =>
    for each callback in `ReactCurrentActQueue.current`
      (e.g.) flushSyncCallbacks =>
        for each callback in `syncQueue` =>
          (e.g.) performSyncWorkOnRoot => ...



performSyncWorkOnRoot(FiberRoot) =>
  getNextLanes
  renderRootSync(FiberRoot, Lanes) =>
    set RenderContext as executionContext global
    pushDispatcher (set ContextOnlyDispatcher as ReactCurrentDispatcher global)
    prepareFreshStack(FiberRoot, Lanes) =>
      createWorkInProgress => createFiber
    workLoopSync =>
      while workInProgress
        performUnitOfWork(workInProgress) =>
          beginWork(Firber.alternate (aka current), Fiber (aka workInProgress)) =>
            switch by workInProgress.tag
            (if HostRoot)
              updateHostRoot =>
                pushHostRootContext (set host implementation globals)
                processUpdateQueue =>
                  getStateFromUpdate(Fiber, UpdateQueue, Update) =>
                    switch by Update.tag
                    (if UpdateState)
                      return `update.payload` (this case `{ element: ReactElement for `App` }`)
                  set newState to `UpdateQueue.baseState` and `workInProgress.memoizedState`
                (if FiberRoot.isDehydrated)
                  enterHydrationState =>
                    set global `isDehydrating`, `nextHydratableInstance` (cf. ReactFiberHostConfig.getFirstHydratableChildWithinContainer) etc...
                  mountChildFibers => ...
                  set child fiber flag `Hydrating`
                (otherwise)
                  reconcileChildren (reconcile `workInProgress.memoizedState.element` as new children) => reconcileChildFibers => ...
                return workInProgress.child (i.e. newly reconciled fiber)

            (if IndeterminateComponent)
              mountIndeterminateComponent =>
                renderWithHooks =>
                  set currentlyRenderingFiber
                  set ReactCurrentDispatcher to HooksDispatcherOnMount (or HooksDispatcherOnUpdate if `current` Fiber exists)
                  Component(props, ...) (where Component = `workInProgress.type` and props = `workInProgress.pendingProps`)
                    => App ...
                  reset ReactCurrentDispatcher to ContextOnlyDispatcher
                  return children (ReactNode returned from Component) (here `App` returns a number `0`, which to be rendered as a text)
                reconcileChildren => mountChildFibers => ...

            (if FunctionComponent (i.e. rerendering the result of mountIndeterminateComponent))
              updateFunctionComponent =>
                renderWithHooks => ...
                reconcileChildren

            (if HostComponent)
              updateHostComponent =>
                (if current === null (i.e. initial rendering))
                  tryToClaimNextHydratableInstance =>
                    (if not `isHydrating`) return
                    tryHydrate =>
                      (if HostComponent)
                        ReactHostConfig.canHydrateInstance
                        set Fiber.stateNode to hydrated dom instance
                        getFirstHydratableChild as nextHydratableInstance
                      (if SuspenseComponent) TODO
                reconcileChildren

        set next `workInProgress` if `beginWork` returns new `Fiber` (e.g. `HostRoot` returning `IndeterminateComponent`)
        otherwise completeUnitOfWork =>
          loop
            completeWork =>
              switch by `Fiber.tag`
              (if `HostText` (e.g. a text node "0" rendered by `App`)
                getRootHostContainer
                ReactFiberHostConfig.createTextInstance
              (if `HostComponent`)
                (if not first render (i.e. "current" Fiber is not null)
                  updateHostComponent (defined in ReactFiberCompleteWork.new.js different from the one in `beginWork`) => TODO
                (otherwise)
                  (if wasHydrated)
                    prepareToHydrateHostInstance =>
                      ReactDOMHostConfig.hydrateInstance(fiber.stateNode (i.e. dom node), ...) =>
                        precacheFiberNode (inject fiber as "__reactFiber$..." property)
                        updateFiberProps (inject props as "__reactProps$..." property)
                        diffHydratedProperties (warn difference of fiber props and server-side-rendered attributes on dom node)
                  (otherwise)
                    ReactFiberHostConfig.createInstance

            continue loop with `Fiber.siblings` or `Fiber.return` (i.e. parent) if any

  commitRoot => commitRootImpl =>
    set CommitContext
    scheduleCallback (for flushPassiveEffects which includes `useEffect` callbacks)
    commitBeforeMutationEffects =>
      ReactFiberHostConfig.prepareForCommit
      ...
    commitMutationEffects => commitMutationEffects_begin =>
      for each Fiber in Fiber.deletions
        commitDeletion =>
          unmountHostComponents =>
            loop child fibers of unmounted fiber (traverse from parent to children)
              (if HostComponent/HostText)
                ReactHostconfig.removeChildFromContainer (NOTE: `useEffect` cleanup is called after DOM is removed in `flushPassiveEffects`)
              (otherwise)
                commitUnmount =>
                  (if FunctionComponent)
                    loop Effect with `HookInsertion` or `HookLayout` tag (aka `useInsertionEffect`)
                      call Effect.destroy
      commitMutationEffects_complete => commitMutationEffectsOnFiber =>
        swith by `Fiber.flags`
        (if Placement) commitPlacement => ... => ReactFiberHostConfig.appendChildToContainer
        (if Hydrating) unset `Hydrating` flag
        (if Update) commitWork =>
          (if FunctionComponent)
            commitHookEffectListUnmount (hook flags `HookInsertion | HookHasEffect` (aka `useInsertionEffect`))
            commitHookEffectListMount
          (if HostComponent)
            ReactFiberHostConfig.commitUpdate
          (if HostRoot)
            (if FiberRoot.isDehydrated)
              unset FiberRoot.isDehydrated
              ReactFiberHostConfig.commitHydratedContainer
    ReactFiberHostConfig.resetAfterCommit
    commitLayoutEffects => ... => commitLayoutEffectOnFiber =>
      (if HostComponent)
        ReactFiberHostConfig.commitMount


flushPassiveEffects => flushPassiveEffectsImpl =>
  set CommitContext
  commitPassiveUnmountEffects =>
    for each Fiber in Fiber.deletions (i.e. deleted children fibers)
      commitPassiveUnmountEffectsInsideOfDeletedTree_begin =>
        commitPassiveUnmountInsideDeletedTreeOnFiber => commitHookEffectListUnmount
    commitPassiveUnmountEffects_begin =>
      commitPassiveUnmountEffects_complete => commitPassiveUnmountOnFiber =>
        commitHookEffectListUnmount (call `Effect.destroy` callback if any)
  commitPassiveMountEffects => ... => commitPassiveMountOnFiber =>
    (if FunctionComponent)
      commitHookEffectListMount (only for hook flags `HookPassive | HookHasEffect`) =>
        for each `Effect` in a linked list `Fiber.updateQueue.lastEffect`
          call `Effect.create` (i.e. UseEffect callback) and asssign result to `Effect.destroy`
  flushSyncCallbacks
  • component/hook level
(mount)
beginWork => mountIndeterminateComponent => renderWithHooks => App =>
  React.useState => HooksDispatcherOnMountInDEV.useState =>
    mountState =>
      mountWorkInProgressHook (initialize `Hook` and set to `workInProgressHook` and `currentlyRenderingFiber.memoizedState`)
      assign initial state to Hook.memoizedState
      initialize `UpdateQueue` and assign it to `Hook.queue`
      return [Hook.memoizedState, dispatchSetState (bound with `currentlyRenderingFiber` and `UpdateQueue`)]

  React.useEffect => HooksDispatcherOnMountInDEV.useEffect => mountEffect =>
    mountEffectImpl =>
      mountWorkInProgressHook (initialize `Hook` and append to a linked list `workInProgressHook` except the 1st hook call)
      add `Passive` flag to currentlyRenderingFiber.flags
      pushEffect =>
        initialize `Effect` and push to `currentlyRenderingFiber.updateQueue.lastEffect`
        here `Effect.flags` is `HookPassive | HookHasEffect`
      assign effect to Hook.memoizedState


(setState)
dispatchSetState =>
  (NOTE: this routine looks very similar to `updateContainer` in `ReactUpdateQueue.new.js` but `Update` and `UpdateQueue` here are completely independent types from the ones used in `updateContainer`)
  initialize `Update` and push to `Hook.queue`
  scheduleUpdateOnFiber


(update)
beginWork => updateFunctionComponent => renderWithHooks => App =>
  React.useState => HooksDispatcherOnMountInDEV.useState => updateState =>
    updateReducer(basicStateReducer, ...) =>
      updateWorkInProgressHook (restore `workInProgressHook` from `currentlyRenderingFiber.alternate.memoizedState`)
      for each `Update` in `Hook.baseQueue` (there can be multiple `setState` calls)
        reducer(Hook.baseState, action)
        return [hook.memoizedState, hook.queue.dispatch]

  React.useEffect => HooksDispatcherOnMountInDEV.useEffect => updateEffect =>
    updateEffectImpl =>
      updateWorkInProgressHook (restore `Hook` from a linked list `workInProgressHook`)
      pushEffect (but without `HookHasEffect` flag if "deps" are same as previous render)
  • ref (cf. Ref in ReactFiberFlags.js)
(mount)
React.useRef => mountRef => return { current: (initialValue) }


(update)
React.useRef => updateRef => return Hook.memoizedState


commitLayoutEffectOnFiber => commitAttachRef =>
  instance = Fiber.stateNode (i.e. dom node)
  (if Fiber.tag is HostComponent) ReactFiberHostConfig.getPublicInstance(instance)
  (if Fiber.ref is function)
    call ref(instance)
  (otherwise)
    set ref.current = instance


commitDeletionEffectsOnFiber, commitMutationEffectsOnFiber, etc.. => safelyDetachRef => (call with null or set null)
  • reconcilation
reconcileChildFibers =>
  (simple case where `currentFirstChild: null` and `newChild: ReactElement`)
    reconcileSingleElement =>
      createFiberFromElement =>
        createFiberFromTypeAndProps (new `Fiber` with `IndeterminateComponent` tag and `App` type)
    placeSingleChild
  • data structure
ReactElement
  type, key, ref, props

FiberRoot
  current: Fiber

Fiber
  tag
  type
  flags (lifecycle flag e.g. Placement Update, Deletion)
  pendingProps
  updateQueue
  child
  return
  sibling
  stateNode
  deletions (cf. deleteChild in ReactChildFiber.new.js)
  memoizedState

Lane (bits contstants in react-reconciler/src/ReactFiberLane.new.js)

ExecutionContext (RenderContext, CommitContext, ...)

Hook
  baseState, memoizedState
  queue: UpdateQueue
    dispatch
    lastRenderedReducer (basicStateReducer by default)
    lastRenderedState

Effect
  create (callback passed via UseEffect)
  destroy (return value of `create`)
  • global states
(react-reconciler/src/ReactFiberWorkLoop.new.js)
executionContext: ExecutionContext
workInProgressRoot: FiberRoot

(react-reconciler/src/ReactFiberSyncTaskQueue.new.js)
syncQueue: Array<SchedulerCallback>

(react-reconciler/src/ReactFiberHooks.new.js)
currentlyRenderingFiber
workInProgressHook
currentHook

(react/src/ReactSharedInternals.js)
ReactCurrentDispatcher
ReactCurrentActQueue (only for testing?)

react-dom

  • host config
  • synthetic event system
    • register event listener
    • event dispatch
    • global ReactCurrentBatchConfig
    • global currentUpdatePriority (reconciler Lane)
  • server side rendering
    • hook state (cf. ReactDOMServerIntegrationHooks-test.js)
    • hydration (cf. ReactServerRenderingHydration-test.js)
  • suspense
# debug through legacy root api
yarn test --debug packages/react-dom/src/__tests__/ReactDOM-test.js -t 'should bubble onSubmit'
# entrypoint
ReactDOM.render =>
  legacyRenderSubtreeIntoContainer (forceHydrate = false) =>
    legacyCreateRootFromDOMContainer =>
      createContainer (HTMLElement as `containerInfo`) [react-reconciler]
      listenToAllSupportedEvents =>
        for each event in `allNativeEvents`
          listenToNativeEvent (on root HTMLElement) =>
            addTrappedEventListener =>
              createEventListenerWrapperWithPriority =>
                getEventPriority (hard coded mapping of priorities)
                choose global event handler based on priority (e.g. dispatchDiscreteEvent)
              addEventListener on root container
    flushSync => updateContainer [react-reconciler]


# render
ReactFiberHostConfig.createInstance =>
  createElement (document.createElement with a few tricks)
  updateFiberProps (sneak props into HTMLElement as "__reactProps...")


# browser event dispatch
dispatchDiscreteEvent =>
  ReactCurrentBatchConfig.transition = 0
  setCurrentUpdatePriority(DiscreteEventPriority)
  dispatchEvent =>
    dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay =>
      findInstanceBlockingEvent (find relevant Fiber) =>
        getEventTarget(nativeEvent)
        getClosestInstanceFromNode (get Fiber from HTMLElement)
        set found Fiber to return_targetInst global
      dispatchEventForPluginEventSystem(... return_targetInst ...) =>
        batchedUpdates => dispatchEventsForPlugins =>
          extractEvents => SimpleEventPlugin.extractEvents(dispatchQueue, ...) =>
            (e.g. if "click" event)
            topLevelEventsToReactNames (map to "onClick")
            get constructor SyntheticMouseEvent
            accumulateSinglePhaseListeners (collect `onClick` props from target Fiber to root Fiber)
            push SyntheticMouseEvent to dispatchQueue
          processDispatchQueue =>
            processDispatchQueueItemsInOrder =>
              (if `stopPropagation` is already called) return
              (otherwise) executeDispatch =>
                invokeGuardedCallbackAndCatchFirstError (calling handler with try/catch)

server side rendering

yarn test --debug packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js -t 'basic render'
yarn test --debug packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js -t 'should warn when the style property differs'
------------
-- render --
------------

renderToStringImpl(ReactNodeList, ...) =>
  createResponseState
  createRequest(ReactNodeList, ...) => createTask
  startWork => scheduleWork => performWork =>
    set ReactCurrentDispatcher to Dispatcher in ReactFizzHooks.js (notably `useEffect` is noop)
    retryTask(Request, Task) =>
      renderNodeDestructive =>
        (if REACT_ELEMENT_TYPE)
          renderElement =>
            # switch by type
            (if functional component)
              renderIndeterminateComponent =>
                renderWithHooks (defined in ReactFizzzServer.js. this is not the one from react-conciler) =>
                  prepareToUseHooks
                  Component => ...
                  finishHooks
                renderNodeDestructive (recursively for returned element)
            (if dom)
              renderHostElement =>
                pushStartInstance (emit open tag with props as attributes (except `children`))
                renderNode (for children) => renderNodeDestructive
                pushEndInstance (emit close tag)
            (if REACT_SUSPENSE_TYPE) TODO
        (if REACT_LAZY_TYPE) TODO
  startFlowing => flushCompletedQueues


------------------------------
-- hook (ReactFizzzHook.js) --
------------------------------

useState => useReducer =>
  createWorkInProgressHook
  (if isReRender)
    TODO: is this relevant only when using `Suspense`?
  (otherwise)
    evaluate initial state


------------------------------------------------
-- hydration (see beginWork/commitWork above) --
------------------------------------------------

ReactDOM.hydrate =>
  legacyRenderSubtreeIntoContainer (forceHydrate = true) =>
    legacyCreateRootFromDOMContainer(..., forceHydrate) =>
      (if not forceHydrate) clear children dom
      createContainer (create FiberRoot with isDehydrated = true)


--------------------
-- data structure --
--------------------

Request
  Destination
  pingedTasks Array<Task>

Task
  ReactNodeList
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment