In-depth study of React principles

Originally published in my newsletter.

React v19 beta has been released. Compared to React 18, it offers many user-friendly APIs, though its core principles remain largely unchanged. You might have been using React for a while, but do you know how it works under the hood?

This article will help you building a version of React with about 400 lines of code that supports asynchronous updates and can be interrupted — a core feature of React upon which many higher-level APIs rely. Here is a final effect Gif:

I used the tic-tac-toe tutorial example provided by React’s official website and can see that it works well.

It is currently hosted on my GitHub, and you can also visit the Online Version to try it out for yourself.

GitHub – ZacharyL2/mini-react: Implement Mini-React in 400 lines of code, a minimal model with asynchronous interruptible updates.

JSX and createElement

Before diving into the principles of mini-react.ts, it’s important to understand what JSX represents. We can use JSX to describe the DOM and easily apply JavaScript logic. However, browsers don’t understand JSX natively, so our written JSX is compiled into JavaScript that browsers can understand.

You can see that it calls React.createElement, which provides the following options:

type: Indicates the type of the current node, such as div.config: Represents the attributes of the current element node, for example, {id: “test”}.children: Child elements, which could be multiple elements, simple text, or more nodes created by React.createElement.

If you are a seasoned React user, you might recall that before React 18, you needed to import React from ‘react’; to write JSX correctly. Since React 18, this is no longer necessary, enhancing developer experience, but React.createElement is still called underneath.

For our simplified React implementation, we need to set react({ jsxRuntime: ‘classic’ }) when configuring Vite.

Then we can implement our own:

Render

Next, we implement a simplified version of the render function based on the data structure created earlier to render JSX to the real DOM.

Here is the online implementation link. It currently renders the JSX only once, so it doesn’t handle interaction.

Fiber architecture and concurrency mode

Fiber architecture and concurrency mode were mainly developed to solve the problem where once a complete element tree is recursed, it can’t be interrupted, potentially blocking the main thread for an extended period. High-priority tasks, such as user input or animations, might not be processed timely.

In React’s source code, work is broken into small units. Whenever the browser is idle, it processes these small work units, relinquishing control of the main thread to allow the browser to respond to high-priority tasks promptly. Once all the small units of a job are completed, the results are mapped to the real DOM.

Two key points are how to relinquish the main thread and how to break the work into manageable units.

requestIdleCallback

requestIdleCallback is an experimental API that executes a callback when the browser is idle. It’s not yet supported by all browsers. In React, it is used in the scheduler package, which has more complex scheduling logic than requestIdleCallback, including updating task priorities.

But here we only consider asynchronous interruptibility, so this is the basic implementation that imitates React:

Here’s a brief explanation of some key points:

Why use MessageChannel?

Primarily, it uses macro-tasks to handle each round of unit tasks. But why macro-tasks?

This is because we need to use macro-tasks to relinquish control of the main thread, allowing the browser to update the DOM or receive events during this idle period. As the browser updates the DOM as a separate task, JavaScript is not executed at this time.

The main thread can only run one task at a time — either executing JavaScript or processing DOM calculations, style computations, input events, etc. Micro-tasks, however, do not relinquish control of the main thread.

Why not use setTimeout?

This is because modern browsers consider nested setTimeout calls more than five times to be blocking and set their minimum delay to 4ms, so it is not precise enough.

Algorithm

Please note, React continues to evolve, and the algorithms I describe may not be the latest, but they are sufficient to understand its fundamentals.

This is a primary reason why the React package is so large.

Here’s a diagram showing the connections between work units:

In React, each work unit is called a Fiber node. They are linked together using a linked list-like structure:

child: Pointer from the parent node to the first child element.return/parent: All child elements have a pointer back to the parent element.sibling: Points from the first child element to the next sibling element.

With this data structure in place, let’s look at the specific implementation.

We’re simply expanding the render logic, restructuring the call sequence to workLoop -> performUnitOfWork -> reconcileChildren -> commitRoot.

workLoop : Get idle time by calling requestIdleCallback continuously. If it is currently idle and there are unit tasks to be executed, then execute each unit task.performUnitOfWork: The specific unit task performed. This is the embodiment of the linked list idea. Specifically, only one fiber node is processed at a time, and the next node to be processed is returned.reconcileChildren: Reconcile the current fiber node, which is actually the comparison of the virtual DOM, and records the changes to be made. You can see that we modified and saved directly on each fiber node, because now it is just a modification to the JavaScript object, and does not touch the real DOM.commitRoot: If an update is currently required (according to wipRoot) and there is no next unit task to process (according to !nextUnitOfWork), it means that virtual changes need to be mapped to the real DOM. The commitRoot is to modify the real DOM according to the changes of the fiber node.

With these, we can truly use the fiber architecture for interruptible DOM updates, but we still lack a trigger.

Triggering Updates

In React, the most common trigger is useState, the most basic update mechanism. Let’s implement it to ignite our Fiber engine.

Here is the specific implementation, simplified into a function:

It cleverly keeps the hook’s state on the fiber node and modifies the state through a queue. From here, you can also see why the order of React hook calls must not change.

Conclusion

We have implemented a minimal model of React that supports asynchronous and interruptible updates, with no dependencies, and excluding comments and types, it might be less than 400 lines of code. I hope it helps you.

If you find this helpful, please consider subscribing to my newsletter for more insights on web development. Thank you for reading!

Build Your Own React.js in 400 Lines of Code was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.

​ Level Up Coding – Medium

about Infinite Loop Digital

We support businesses by identifying requirements and helping clients integrate AI seamlessly into their operations.

Gartner
Gartner Digital Workplace Summit Generative Al

GenAI sessions:

  • 4 Use Cases for Generative AI and ChatGPT in the Digital Workplace
  • How the Power of Generative AI Will Transform Knowledge Management
  • The Perils and Promises of Microsoft 365 Copilot
  • How to Be the Generative AI Champion Your CIO and Organization Need
  • How to Shift Organizational Culture Today to Embrace Generative AI Tomorrow
  • Mitigate the Risks of Generative AI by Enhancing Your Information Governance
  • Cultivate Essential Skills for Collaborating With Artificial Intelligence
  • Ask the Expert: Microsoft 365 Copilot
  • Generative AI Across Digital Workplace Markets
10 – 11 June 2024

London, U.K.