Introduction
When you find yourself struggling to grasp React development skills from the documentation, there's no better way to deeply understand React's core concepts than by building your own version of React.
In this series, I'll guide you step by step on how to build React from scratch. You'll personally implement the core concepts of React from 2013 (React 0.3) to 2019 (React 16.8), gaining a profound understanding of its design principles and the trade-offs made during the evolution of the technology.
Overview
This series will be divided into three articles, The features we'll implement include:
I: Basic Feature Implementation
Render: Introduced in React 0.13 (2015)
, the render function is used to render the virtual DOM to the real DOM.
JSX: Also introduced in React 0.13 (2015)
, JSX syntax provides a convenient way to write component structures and converts it into calls to the createElement function.
II: Enhanced Core Features
VDOM: In React 0.3 (2013)
, the concept of virtual DOM was introduced, used to compare and update the real DOM to improve performance.
Event Binding: Also introduced in React 0.3 (2013)
, event binding functionality allows components to respond to user interactions.
Task Scheduler & Fiber: Introduced in React 16 (2017)
, the Fiber architecture is a redesigned scheduler used to implement priority scheduling of component updates, preventing render blocking.
III: Advanced Features and State Management
Diff Algorithm: In React 0.3 (2013)
, the diff algorithm based on virtual DOM was introduced, used to compare differences in virtual DOM to minimize DOM operations and improve performance.
Function Component: In React 16.8 (2019)
, Hooks were introduced, including useState and useEffect, as well as the implementation of function components.
useState: Introduced in React 16.8 (2019)
, the useState hook allows function components to have their own state.
useEffect: Introduced in React 16.8 (2019)
, the useEffect hook is used to handle component side effects such as data fetching and subscriptions.
let's dive into Basic Feature Implementation
Step One: render
Let's create a simple React renderer step by step and gradually optimize it. Firstly, We'll start with a basic HTML file and then progressively introduce React's core concepts.
1. Create an HTML File
First, let's create a basic HTML file that includes a root div element with an id of "root". This will be the element where we render our React components.
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scal=1.0">
<title>Simple React Renderer</title>
</head>
<body>
<div id="root"></div>
<script src="./main.js" type="module"></script>
</body>
</html>
// main.js
const dom = document.createElement("div");
dom.id = "app";
document.querySelector("#root").append(dom);
Here, we have simply mounted the app. Next, we need to add some React elements inside this container, and we'll need a createElement function for that.
2. Create the createElement Function
We'll start by implementing the createElement function. This function is used to create React elements. It accepts three parameters:
- type (the element type)
- props (the properties)
- children (the child elements).
Props is introduced in React 0.3 (2013)
, it's an object containing all the attributes passed to the component. allowing components to receive data from parent components.
// Create the createElement function
function createElement(type, props, ...children) {
return {
type, // Element type
props: {
...props, // Properties
children: children.map(child =>
typeof child === 'object'
? child
: createTextElement(child)
),
},
};
}
Create the createTextElement Function We also need a function to create text node elements.
// Create a text node
function createTextElement(text) {
return {
type: 'TEXT_ELEMENT',
props: {
nodeValue: text,
children: [],
},
};
}
3. Create the render Function
Next, we'll write a simple render function that will render content into a container based on the React element passed in.
// Render function
export function render(element, container) {
if (typeof element.type === 'function') {
const component = element.type;
const renderedElement = component(element.props);
render(renderedElement, container);
return;
}
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)
);
console.log('dom: ', dom);
container.appendChild(dom);
}
4. Create the Root Node and Render the React Element
Finally, we'll get the root node and render the created React element into the root node.
// Create the root node
const root = document.getElementById('root');
// Create a React element
const element = createElement('div', { id: 'app' }, 'Hello, world!');
// Render the React element to the root node
render(element, root);
Let's print the content of the app and see that it matches our virtual DOM
Now, we have successfully implemented a simple React renderer using vanilla JavaScript that can render a div element containing text to the page.
We have slightly reorganized the files into the following structure:
├── src
│ ├── App.js
│ ├── core
│ │ ├── createElement.js
│ │ ├── reactDom.js
│ │ └── render.js
│ ├── index.html
│ └── index.js
You can explore the reorganized code here on GitHub.
Step Two: JSX
With the code above, we have completed a simple mini-react renderer. However, we usually use JSX to simplify the creation of React elements.
JSX is a syntax extension that looks similar to HTML and allows us to write XML-like code inside JavaScript. Since browsers can't understand JSX directly, we need to transform it into pure JavaScript code.
To use JSX here, we need to leverage some libraries, such as Webpack, Babel, or Vite.
Here's a brief comparison of these libraries for compiling JSX:
Feature | JSX Compilation | Speed | Configuration |
---|---|---|---|
Webpack | Uses Babel or similar loaders/plugins | Slower due to additional configuration | Requires setting up Babel loader |
Babel | Directly compiles JSX | Fast for compilation | Requires configuration |
Vite | Built-in support, uses ESBuild | Very fast, optimized for development | Minimal setup, almost zero configuration |
In this case, we'll use Vite. Vite is a modern build tool focused on a fast development experience. It has built-in support for JSX and uses ESBuild for fast builds. It can directly transform JSX into JavaScript without additional configuration.
1. Configure Vite
First, ensure you have Vite installed. Then, create a vite.config.js file in your project root directory and add the following configuration:
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
root: 'src',
esbuild: {
jsxFactory: 'createElement',
jsxFragment: 'Fragment',
},
});
Here, we tell Vite to use MiniReact.createElement as the factory function for JSX elements.
Now, we can create a file using JSX and use MiniReact.createElement to create React elements.
Create a JSX file for your app:
// src/App.jsx
import { createElement } from './core/createElement';
function App() {
return (
<div id="app">
Hello, world!
<NestedComponent />
</div>
);
}
function NestedComponent() {
return (
<div>
This is a nested component.
</div>
);
}
export default App;
Then open your browser and visit http://localhost:3000, and you'll see your React application rendered on the page using JSX!
Summary
In the first article, we've laid the groundwork for understanding and implementing the core principles of React.
In the next part of our series, we will delve into the enhanced core features of React, including the Virtual DOM, event binding, and the innovative Fiber architecture.
The next part is coming soon: Learning Core Features: A Deep Dive into the Principles through Building Mini-React (II) 😊