Concurrent React From Scratch

React @ ReactAdvanced London
Oct 24 2019 ( External link )

Description: In this talk, we’ll create an effective mental model of Concurrent React by building a tiny clone of React! We will start with a blank js file and learn about how React renders components, schedules Time-Slicing updates with a Work Loop, add Hooks, and end off with a mini-clone of Suspense!

React Knowledgeable version

I gave a 1hr Singlished version of this talk, with a lot more mistakes but hopefully better explanations, and Q&A, at Shopee on Nov 29.

Final Codesandbox

Click here: https://codesandbox.io/s/reactadvanced-final-uwrx0

the crazy plan

  • start state
import './styles.css'
import {
  reconcileChildren,
  createElement,
  commitDeletion,
  createDom,
  updateDom
} from './utils'
let nextUnitOfWork = null
let currentRoot = null
let wipRoot = null
let deletions = []
let wipFiber = null
let hookIndex = null
const React = { createElement }
  • declare with an element and a wipRoot
const container = document.getElementById('root')
const element = <h1>Hello world</h1>
wipRoot = {
  // type: 'n/a', // a string or function
  dom: container,
  props: {
    children: [element]
  }
  // // links
  // alternate - pending fiber
  // child - link to first child
  // parent - link to parent
  // sibling - link to next sibling
}
// traversal: https://github.com/facebook/react/issues/7942
  • simple render
render(element, container)
// render(wipRoot.props.children[0], wipRoot.dom);

function render(element, container) {
  const dom =
    element.type === 'TEXT_ELEMENT'
      ? document.createTextNode('')
      : document.createElement(element.type)
  const isProperty = key => key !== 'children'
  Object.keys(element.props)
    .filter(isProperty)
    .forEach(name => {
      dom[name] = element.props[name]
    })
  element.props.children.forEach(
    child => render(child, dom) // recursive call
  )
  container.appendChild(dom)
}
  • talk about fiber traversal

https://github.com/facebook/react/issues/7942

  • reconciling fibers
nextUnitOfWork = wipRoot
while (nextUnitOfWork) {
  nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
}
commitWork(wipRoot.child)

function performUnitOfWork(fiber) {
  const isFunctionComponent = fiber.type instanceof Function
  if (isFunctionComponent) {
    // it is either a function component... (so call it)
    wipFiber = fiber
    hookIndex = 0
    wipFiber.hooks = []
    const children = [fiber.type(fiber.props)]
    reconcileChildren(fiber, children.flat())
  } else {
    // or a host component... (so createDom)
    if (!fiber.dom) fiber.dom = createDom(fiber)
    reconcileChildren(fiber, fiber.props.children.flat())
  }
  if (fiber.child) return fiber.child
  let nextFiber = fiber
  while (nextFiber) {
    if (nextFiber.sibling) return nextFiber.sibling
    nextFiber = nextFiber.parent
  }
}
function commitWork(fiber) {
  if (!fiber) return
  let domParentFiber = fiber.parent
  while (!domParentFiber.dom) {
    domParentFiber = domParentFiber.parent
  }
  const domParent = domParentFiber.dom
  if (fiber.effectTag === 'PLACEMENT' && fiber.dom != null) {
    domParent.appendChild(fiber.dom)
  } else if (fiber.effectTag === 'UPDATE' && fiber.dom != null) {
    updateDom(fiber.dom, fiber.alternate.props, fiber.props)
  } else if (fiber.effectTag === 'DELETION') {
    commitDeletion(fiber, domParent)
  }
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}
  • add simple work loop
// https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
function workLoop(deadline) {
  // reconcile phase
  while (nextUnitOfWork) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
  }
  // commit phase
  if (!nextUnitOfWork && wipRoot) {
    // commitRoot
    deletions.forEach(commitWork)
    commitWork(wipRoot.child)
    currentRoot = wipRoot
    wipRoot = null
  }
  requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
  • add time sliced work loop
function workLoop(deadline) {
  let shouldYield = false
  // reconcile phase
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    shouldYield = deadline.timeRemaining() < 1
  }
  // commit phase
  if (!nextUnitOfWork && wipRoot) {
    // commitRoot
    deletions.forEach(commitWork)
    commitWork(wipRoot.child)
    currentRoot = wipRoot
    wipRoot = null
  }
  requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
  • add useState hook
function useState(initial) {
  const oldHook = wipFiber?.alternate?.hooks[hookIndex]
  const nothing = Symbol('__NONE__')
  const hook = {
    state: oldHook ? oldHook.state : initial,
    pendingState: nothing
  }
  if (oldHook && oldHook.pendingState !== nothing) {
    hook.state = oldHook.pendingState
  }
  const setState = newState => {
    hook.pendingState = newState
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot
    }
    nextUnitOfWork = wipRoot
    deletions = []
  }
  wipFiber.hooks.push(hook)
  hookIndex++
  return [hook.state, setState]
}

w basic demo:

// remember to expose useState!
function App() {
  const [state, setState] = React.useState(1)
  const handler = () => setState(state + 1)
  return (
    <main>
      <button onClick={handler}>Click me: {state}</button>
    </main>
  )
}
const element = <App />
  • add final render method
function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element]
    },
    alternate: currentRoot
  }
  deletions = []
  nextUnitOfWork = wipRoot
}
render(element, container)

with createRoot

function createRoot(container) {
  return {
    render(element) {
      wipRoot = {
        dom: container,
        props: {
          children: [element]
        },
        alternate: currentRoot
      }
      deletions = []
      nextUnitOfWork = wipRoot
    }
  }
}
createRoot(container).render(element)
  • add suspense
// fakeapi from https://codesandbox.io/s/vigorous-keller-3ed2b
import { fetchProfileData } from './fakeApi'
const initialResource = fetchProfileData(0)
function App() {
  const [resource, setResource] = React.useState(initialResource)
  const [state, setState] = React.useState(0)
  const handler = () => {
    let newState = state + 1
    if (newState > 3) newState = 0
    setState(newState)
    setResource(fetchProfileData(newState))
  }
  const user = resource.user.read()
  const posts = resource.posts.read()
  return (
    <main>
      <button onClick={handler}>Beatle {state + 1}</button>
      <h1>{user.name}</h1>
      <div>
        {posts.map(post => (
          <p>{post.text}</p>
        ))}
      </div>
    </main>
  )
}

catch suspender

function workLoop(deadline) {
  // console.log("workloop start");
  let shouldYield = false
  let suspendedWork = null
  // reconcile phase
  while (nextUnitOfWork && !shouldYield) {
    try {
      nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    } catch (err) {
      console.error('caught', err)
      if (err instanceof Promise) {
        suspendedWork = nextUnitOfWork
        nextUnitOfWork = null
        err.then(() => {
          wipRoot = currentRoot
          nextUnitOfWork = suspendedWork
        })
      } else {
        throw err
      }
    }
    shouldYield = deadline.timeRemaining() < 1
  }
  // commit phase
  if (!nextUnitOfWork && wipRoot) {
    // commitRoot
    deletions.forEach(commitWork)
    commitWork(wipRoot.child)
    currentRoot = wipRoot
    wipRoot = null
  }
  requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)

References

first version of plan

the fiber path

  • 15 min react + hooks clone
  • 10 min fiber talk???

the svelte path

  • 15 min react + hooks clone
  • 10 min svelte clone??

other references

{
  "title": "Concurrent React From Scratch",
  "slug": "react-from-scratch",
  "topic": "React",
  "venues": "ReactAdvanced London",
  "url": "https://reactadvanced.com/",
  "video": "https://www.youtube.com/watch?v=dFO4m7Y-yhs",
  "video2": "https://www.youtube.com/watch?v=8opFTK2shAc",
  "date": "2019-10-25T00:00:00.000Z",
  "desc": "Cloning Concurrent React with React Fiber and discussing Time Slicing and Suspense",
  "description": "In this talk, we’ll create an effective mental model of Concurrent React by building a tiny clone of React! We will start with a blank js file and learn about how React renders components, schedules Time-Slicing updates with a Work Loop, add Hooks, and end off with a mini-clone of Suspense!",
  "pubdate": "2019-10-25T00:00:00.000Z"
}