Concurrent React From Scratch

React @ ReactAdvanced London
Oct 24 2019 ( External link )

Description: In this talk, we’ll create an effective mental model of React Hooks by building a tiny clone of React! This will serve two purposes – to demonstrate the effective use of closures, and to show how you can build a Hooks clone in just 29 lines of readable JS. Finally, we arrive at how you get Custom Hooks and the Rules of Hooks out of this incredible mental model!

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 = React.createElement("h1", null, "Hello world");
// const element = <h1>Hello world</h1>
// const element = {
//   type: "h1",
//   props: {
//     children: "Hello world",
//   },
// }
const fiber = {
  type: "h1",
  props: { children: "Hello world" },
  tag: HOST_COMPONENT,
  parent: parentFiber,
  child: childFiber,
  sibling: null,
  alternate: currentFiber,
  effectTag: PLACEMENT,
  hooks: []
};
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

let root = fiber;
let node = fiber;
while (true) {
  // Do something with node
  if (node.child) {
    node = node.child;
    continue;
  }
  if (node === root) {
    return;
  }
  while (!node.sibling) {
    if (!node.return || node.return === root) {
      return;
    }
    node = node.return;
  }
  node = node.sibling;
}
  • 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 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 = _setStateFunction => {
    hook.pendingState = _setStateFunction;
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot
    };
    nextUnitOfWork = wipRoot;
    deletions = [];
  };
  wipFiber.hooks.push(hook);
  hookIndex++;
  return [hook.state, setState];
}

w basic demo:

const React = { useState, createElement };
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 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 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",
  "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 React Hooks by building a tiny clone of React! This will serve two purposes – to demonstrate the effective use of closures, and to show how you can build a Hooks clone in just 29 lines of readable JS. Finally, we arrive at how you get Custom Hooks and the Rules of Hooks out of this incredible mental model!",
  "pubdate": "2019-10-25T00:00:00.000Z"
}