Part 2/3 - Beginners guide to Custom React Renderers. How to build your own renderer from scratch?

This is Part 2 of a 3-part series. Be sure to read Part 1 before proceeding.

This installment covers the initial render phase of the renderer. Part 3 will cover the update phase.

HostConfig

Renderers must implement platform-specific functions within the HostConfig. The reconciler calls different functions during initial render versus update phases triggered by setState.

The example application we'll be rendering:

const Text = (props) => {
  return <p className={props.className}>{props.content}</p>
}

const App = () => {
  return (
    <div>
      <Text className="hello-class" content="Hello" />
      <span style="color:blue;">World</span>
    </div>
  )
}

Essential HostConfig Methods

now()

Used by the reconciler to calculate the current time:

now: Date.now,

getRootHostContext(nextRootInstance)

Returns a context object passed to immediate children. The nextRootInstance is the root DOM node specified during render.

getRootHostContext: function (nextRootInstance) {
  let rootContext = {}
  return rootContext
}

getChildHostContext(parentContext, fiberType, rootInstance)

Provides access to parent context and enables passing context to immediate children.

getChildHostContext: function (parentContext, fiberType, rootInstance) {
  let context = { type: fiberType }
  return context
}

shouldSetTextContent(type, nextProps)

Returns a boolean determining whether text is created inside the host element or separately. When returning true, no separate text element is created and child traversal stops.

shouldSetTextContent: function (type, nextProps) {
  return false
}

createTextInstance(newText, rootContainerInstance, currentHostContext, workInProgress)

Creates an actual text node:

createTextInstance: function(
  newText,
  rootContainerInstance,
  currentHostContext,
  workInProgress
) {
  return document.createTextNode(newText)
}

createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress)

Called on all host nodes except leaf text nodes. Creates the appropriate DOM element and handles props:

createInstance: function(
  type,
  newProps,
  rootContainerInstance,
  currentHostContext,
  workInProgress
) {
  const element = document.createElement(type)
  element.className = newProps.className || ''
  element.style = newProps.style
  return element
}

appendInitialChild(parent, child)

Attaches child DOM nodes to parents during initial render, called for each child of the current node:

appendInitialChild: (parent, child) => {
  parent.appendChild(child)
}

finalizeInitialChildren(instance, type, newProps, rootContainerInstance, currentHostContext)

Returns a boolean determining whether commitMount should be called for the element. Used for operations like autofocus that occur after rendering completes:

finalizeInitialChildren: (
  instance,
  type,
  newProps,
  rootContainerInstance,
  currentHostContext
) => {
  return newProps.autofocus
}

prepareForCommit(rootContainerInstance)

Called when the in-memory render tree is complete but before attachment to the actual DOM:

prepareForCommit: function (rootContainerInstance) {}

resetAfterCommit(rootContainerInstance)

Executes after the in-memory tree attaches to the root DOM element:

resetAfterCommit: function (rootContainerInstance) {}

appendChildToContainer(parent, child)

Attaches the in-memory tree to the root host div. Requires supportsMutation: true:

appendChildToContainer: (parent, child) => {
  parent.appendChild(child)
},
supportsMutation: true,

commitMount(domElement, type, newProps, fiberNode)

Called for elements where finalizeInitialChildren returned true, after all tree attachment completes. Primarily used for autofocus:

commitMount: (domElement, type, newProps, fiberNode) => {
  domElement.focus()
}

Complete HostConfig Implementation

const HostConfig = {
  now: Date.now,
  getRootHostContext: function (nextRootInstance) {
    let rootContext = {}
    return rootContext
  },
  getChildHostContext: function (parentContext, fiberType, rootInstance) {
    let context = { type: fiberType }
    return context
  },
  shouldSetTextContent: function (type, nextProps) {
    return false
  },
  createTextInstance: function (
    newText,
    rootContainerInstance,
    currentHostContext,
    workInProgress
  ) {
    return document.createTextNode(newText)
  },
  createInstance: function (
    type,
    newProps,
    rootContainerInstance,
    currentHostContext,
    workInProgress
  ) {
    const element = document.createElement(type)
    element.className = newProps.className || ''
    element.style = newProps.style
    return element
  },
  appendInitialChild: (parent, child) => {
    parent.appendChild(child)
  },
  finalizeInitialChildren: (
    instance,
    type,
    newProps,
    rootContainerInstance,
    currentHostContext
  ) => {
    return newProps.autofocus
  },
  prepareForCommit: function (rootContainerInstance) {},
  resetAfterCommit: function (rootContainerInstance) {},
  commitMount: (domElement, type, newProps, fiberNode) => {
    domElement.focus()
  },
  appendChildToContainer: (parent, child) => {
    parent.appendChild(child)
  },
  supportsMutation: true,
}

With these implementations, a functional custom React renderer capable of rendering JSX to the DOM is achieved.

Continue to Part 3/3 where we handle the update phase.

References

This post was ported over from Atul's old blog. Read the original here.