Giter Site home page Giter Site logo

react-16-clone's Introduction

Building React 16 (16.8) from scratch

The purpose of this project is to gain a deeper understanding on how React works internally.

Features this bare bone React version includes, in order of development:

  1. createElement function
  2. render function
  3. Concurrent Mode
  4. Fibers (VDOM building tool)
  5. Render and Commit phases
  6. Reconciliation
  7. Support for Function components
  8. Support for hooks
  9. This minified version of React DOES NOT support class components

Based on an article published by Rodrigo Pombo back in 2019.

Development notes

How is JSX transpiled to JS?

Given the following JSX code:

const element = <h1 title="foo">Hello, world!</h1>

What is happening in the background is that React is calling its createElement function internally:

const element = React.createElement(
  "h1", // HTML tag
  { title: "foo" }, // HTML Attributes
  "Hello" // Content / Children
)

The createElement function creates an object from its arguments, that would look like this:

const element = {
  type: "h1",
  props: {
    title: "foo",
    children: "Hello",
  },
}

This is, in summary, what a React element is: an object with two properties: type and props.

children in this case is a string, but usually it is an array with more React elements (objects with type, props, and children as well). So React elements have a tree structure, in which there's a root element with one or more elements anidated, and this goes on ad-infinitum.

Creating a custom createElement function

The createElement function creates React elements from a set of properties that define HTML nodes. This elements are objects disposed in a tree structure that makes the VDOM easier to compose.

This function accepts a type (HTML tag), some props (HTML attributes and custom props), and an array of children, that will be returned inside the props object in the result element (or empty array if none have been passed).

It has to be taken into account that sometimes children are primitive values, so a helper function createTextElement was created to be called when a children is not of type object.

In the real world, React doesn’t wrap primitive values or create empty arrays when there are no children, but in this case it was done like this to simplify the code.

Creating a custom React-like API that Babel can use to transpile JSX

This custom clone of React is called RReact (quite original).

But we still want to use JSX here. How do we tell Babel to use RReact’s createElement instead of the original React’s function?

We can add a comment to our code to tell Babel to use our function instead of React's one when transpiling JSX:

/** @jsx RReact.createElement */
const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
)

We don't need the same comment for the render function. This is because the babel/preset-react plugin included with react-scripts has some default rules for transforming JSX into JS, and one of them is the "Automatic runtime" feature, which uses React as the default API, in order to avoid the need of explicit imports (not needed from React v17). render function comes from ReactDOM, and there's no default runtime for that API, so we can safely use ours instead.

It works! 🤖

At this point in the commit history, the first tiny version of the project should be working fine. In order to be sure the custom RReact API is being used behind the scenes, notice that we didn't import React/ReactDOM into our index.js. Also, checking the build files generated by Webpack inside the browser tools "Sources" tab, should make clear that the functions used during the transpilation were the RReact ones.

Notice that no webpack or babel config was added to the project, nor any related plugins. This is because the (now deprecated) react-scripts library has all those things built-in and pre-configured by default for running React apps with a minimal setup. There are alternatives to expose/customize the webpack and transpiler options, like ejecting or using a third-party library like craco. Nowadays most configurations are exposed OOB when building React apps with frameworks like NextJS or Vite.

Concurrent mode and Fibers

The current implementation has a problem: the render function builds the whole tree synchronously. This means the main thread is blocked until the whole tree has been built. This is not ideal.

In order to avoid this, it is possible to divide the work into small chunks and let the browser interrupt the rendering everytime a new piece of work is finished.

For that, a requestIdleCallback function can be used to control a loop that builds chunks of the tree and is only called whenever the main thread is idle.

React doesn't use a requestIdleCallback anymore. Now it uses the Scheduler package. But for this use case is conceptually the same.

Fibers

Each fiber is a React element and also a unit of work. The root element (fiber) is set as the first nextUnitOfWork, and then the performUnitOfWork function will perform the work and also find and return the next unit of work, until the tree has been built. In order to easily find the nextUnitOfWork, each fiber also has a link to its first child, its next sibling and its parent.

The way to find the next unit of work is the following:

  • If the current fiber has any child, that will be the next unit of work.
  • If not, check if the fiber has any sibling, and assign it as the next unit of work.
  • If there are no children nor siblings, the fiber parent's next sibling will be the next unit of work.
  • If at some point we reach the root fiber, it means the tree building has been completed.

Render and Commit phases

The solution to the previous problem caused another one. Now a new node is added to the DOM each time a unit of work is finished. Since the browser can interrupt that work before the whole tree has been rendered, it can happen that the user sees an incomplete UI.

To avoid that, it is necessary to remove the code which adds the element to the DOM until a later phase (commit phase).

For that, we'll keep track of the work in progress root of the tree, and once the whole fiber tree has been built, it will be commited to the DOM in a single step.

Reconciliation

In order to understand what changed from previous render, we need to keep track of the last fiber tree and compare elements received on the current render function to the last fiber tree commited to the DOM.

This means we need to build fibers differently depending on whether they were part of the previous fiber tree or not, and add some tags to the result fibers of the current tree to mark them as "new", "updated" or "removed". Then, update the DOM accordingly.

Updating props is more or less easy since they just need to be added as-is to the new element, but for event listeners the process is a bit more tricky, cause we need to add and remove them using the inner DOM functions .addEventListener and .removeEventListener.

Add support for functional components

The current implementation of createElement does not support transpiling functions. Function components are different than elements in two ways:

  • The fiber from a function component doesn't have a DOM node.
  • The children come from running the function instead of getting them directly from the props.

Now that we support passing functions to the createElement function, the type property can be an HTML tag (string) or a function, so it can be executed with the props in order to get the children. The rest of the implementation remains the same.

The only other change needed is a couple of extra checks when commiting the work. Since function components doesn't have a DOM node associated, it is necessary to go up the tree an indeterminate number of levels to find the first parent fiber which has a DOM node associated. This is necessary for appending elements but also for deleting them, since it is necessary to delete the DOM node from the tree.

TODO

Out of the scope of the article but interesting things to add in the future

  • Refactor to extract logic from the index.jsx file
  • Add support for style object prop
  • Add useEffect hook
  • Reconciliation by key
  • Flatten children arrays
  • Support for TypeScript

Troubleshooting

Notes for anyone who wants to run the project (most likely myself in several months, if so).

Starting local dev environment for newer Node versions

The libraries used in this tiny project are way outdated even at the moment of developing it. If you get an error Error: error:0308010C:digital envelope routines::unsupported while running the start command (npm start), run this one instead:

NODE_OPTIONS=--openssl-legacy-provider npm start

react-16-clone's People

Contributors

ricardomarsanc avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.