Cover photo by La-Rel Easter – person piling blocks
Welcome to the Babbel Bytes blog series on how to “Build your own (simple) React from scratch!”. In this series, we will be taking a deep dive into the intricacies of React and how to build your own version of it! This series is based on a workshop originally created for React Day Berlin 2022.
React is a popular JavaScript library that is used to build user interfaces for web applications. It allows developers to create complex and dynamic interfaces by breaking them down into smaller, reusable components.
In this blog series, we will take you through the process of building a simple version of React from the ground up. Some basic understanding of JavaScript is all that’s needed; React knowledge (or a similar UI library) will help but it’s not a necessity. We will cover the core concepts of React, such as rendering components and managing state, and provide hands-on examples to help you learn by doing.
We took a “reverse-engineering” approach to building our React library, meaning that we started from our own understanding of React and built things without ever looking at the React codebase. That means our library is quite different to how the real React is built. But don’t worry, the end results are the same and we will end our series comparing the differences in our approaches, dissecting the pros and cons of both.
How the blog series will be segmented
- Introduction and Rendering Components (you are here)
- The holy Virtual DOM: what and why?
- Let’s get Stateful: useState
- Make it fast: Reconciliation and Patching
- Special Effects Time: useEffect
- Looking back on the journey: Summary and comparisons with React
- Bonus – Tying the last knot: JSX parsing
Stay tuned for the next articles in this series where we will dive deeper into the building blocks of React, whilst providing you with hands-on examples to help you learn by doing. But now it’s time to get to work with our very first task – rendering components.
Time to render
Throughout this series, we will be referencing the repository we used for the original React Day Berlin 2022 workshop, which you can find here if you want to follow along.
A quick walkthrough of the repository:
- The
todo-app
is the application we will render with our own React library, we will almost never change any code here - The
packages/my-own-react
is our own react library, this is where most of the action will take place - The
docs
folder contains documentation about some of the key concepts we will explore, but most of those will also be available within the articles
On the main
branch, you can see the todo app working with the official React library; you can play around with it to see what is available.
The app allows you to add and remove TODOs with the browser’s localStorage being used to persist state.
The repository is set up so that the todo app’s webpack* config contains an alias for react
to link to my-own-react
, so we can replace all functionalities of React over time. (on the main
branch, we are reexporting everything from React, so while it is using my-own-react
, for now it is the same as using the real React)
* If you are unfamiliar with Webpack, it is the bundler used by the apps created with the create-react-app
tool. In a nutshell, it enables us to transform all our source files into bundles readable by the browser. This, for example, can allow importing SVG files from your JavaScript code.
To follow along, check out the git branch specified at each step. Those checkouts will be explicitly marked within the articles like this very first one:
If following along, it is now time to go to the first branch git checkout chapter-1/step-1
In order to help you with the tasks, we have added a few pointers:
- A snackbar with a recap of what you need to do in the current step
- A traffic light system to help you assess whether your current code matches the expectations (only for the beginning, as over time, with your React being able to do more and more, it will become simple to assess how you are doing)
For our first task, which is about rendering static data, we will be replacing the react-dom
APIs.
We have set up the equivalent to the react-dom
’s API in the my-own-react/dom-handlers module:
const createRoot = rootElement => ({
rootElement,
render: rootChild => {
// START HERE
// How can we create an element from the rootChild JSX
// and render it to the rootElement
},
});
export const DOMHandlers = {
createRoot,
};
We have created the skeleton structure for rendering by exposing a createRoot
function that takes as an argument an element and returns an object that contains a render
method.
The render
method expects to receive transformed JSX to be rendered as HTML into the element passed to createRoot
.
If you head over to the index of the todo app, you will see we have already set up our code to use our own DOM handlers.
import { DOMHandlers } from 'my-own-react';
const root = DOMHandlers.createRoot(rootElement);
// DON'T FORGET
// You can replace the div here by another tag, for example a span, to make sure your code is fully working!
root.render(<div />);
You can see here we are importing what would traditionally be imported from react-dom
from my-own-react.DOMHandlers
and attempting to render our div
with it…
Now before we try to implement the first version of the render
function, let’s take a step back and talk about JSX. JSX is the most popular/standard way to write React code. It is a convenient format for writing HTML like code while keeping all the power of JavaScript.
If you have encountered JSX before, you might not have stopped to think about what JSX looks like under the hood… but today is different.
If we want to create our own React, we will need to understand how JSX looks once it has been transformed.
To begin with, let’s start by looking into the simplest example: just an HTML tag.
This is what the JSX to render a simple div
looks like:
<div />
This should feel familiar, as it looks similar to HTML.
It’s not very useful but it’s a good place to start.
Now for JSX to work at all, it needs to be transformed into code that can be used in vanilla JavaScript.
To focus only on the core of React, at this point in time we have decided to let the setup from create-react-app
transform it for us (but we will explore this in another post in the series).
After the webpack setup from create-react-app
does its magic, the JSX above, representing a single div
, will be transformed to a data structure which most relevant part for our task will be:
{
type: 'div'
}
Alright, that seems simple enough, let’s try out a different tag:
<span />
will be transformed to
{
type: 'span'
}
We can see a pattern emerging, and that should start giving us an idea of what we need to do.
Now that we know we will receive an object similar to the above in our render
method, we need to think about how we can use that object to create an element to be displayed in the browser’s DOM.
From here, you have all the information you need to have a stab at it yourself, so if you want to code along, you can stop reading here and come back once you get it working.
The first API we will look at is called document.createElement.
It is a method that allows the programmatic creation of HTML elements in JavaScript.
To create a div
element, we can write document.createElement('div')
.
This will create an object in memory that can be later on inserted into the page… which leads us to our next step.
There are many ways to insert elements into the DOM in JavaScript, but for the current case we will use the appendChild API.
So the full code becomes:
const createRoot = rootElement => ({
rootElement,
render: rootChild => {
const rootChildElement = document.createElement(rootChild.type);
rootElement.appendChild(rootChildElement);
},
});
We can’t see much, but you should see the red light system status should have turned orange (meaning we at least covered some of the problem), you can also verify things are working as expected using the developer tools, where you should see a div
within the browser’s DOM – great!
And if we switch the div for a span in the index, and proceed to inspect, we can see the span gets rendered, amazing!
import { DOMHandlers } from 'my-own-react';
const root = DOMHandlers.createRoot(rootElement);
// DON'T FORGET
// You can replace the div here by another tag, for example a span, to make sure your code is fully working!
root.render(<span />);
We are all set for rendering simple HTML tags!
What about props?
A really important part of React (and HTML) is the fact we can pass along props
(in HTML the term attributes
tends to be used) to update the way a specific tag behaves/is displayed/is used. For example, the id
HTML attribute allows you to create a unique ID for a tag.
This looks like this in HTML:
<div id="test"></div>
And would look very similar in JSX:
<div id="test" />
In the case of JSX, it will be transformed by the webpack setup into the following JS structure:
{
type: 'div',
props: { id: 'test' }
}
What if we have more attributes?
{
type: 'div',
props: { id: 'test', tabIndex: 5 }
}
If following along, it is now time to go to the first branch git checkout chapter-1/step-2
You will find more or less what we wrote above in our my-own-react/index.js
, slightly reorganized so that things will be easier to make sense of in the future.
Your task now will be to make sure you are able to interpret the props and pass them correctly to the browser as attributes.
The traffic light system should help you know how you are doing!
If you are coding along, this is where you will attempt to solve this on your own until we continue.
As a first step to get attributes to work, we can create a function to attach attributes to our DOM elements, the simplest version we could write looks like this
const applyPropToHTMLElement = ({ key, value }, element) => {
element[key] = value;
};
const renderTagElementToHtml = ({ props, type }) => {
const domElement = document.createElement(type);
props.forEach(prop => applyPropToHTMLElement(prop, domElement));
return domElement;
};
This works for most props, as they use the same naming as their HTML counterparts, but there are exceptions.
className
is a famous one (the HTML attribute is class
), and stems from the fact that class
is a reserved keyword in JavaScript to create a class in the Object Oriented Programming sense. For the majority of other props, it has been preferred by the React team to use the standard JavaScript camelCase as opposed to the HTML kebab-case, for example in JSX we normally write acceptCharset
as opposed to accept-charset
. While accept-charset
wouldn’t be forbidden in JavaScript, it is generally not practical to write as an object, as it forces developers to always access the key as a string given JavaScript doesn’t support -
in variable names.
Example:
// Camel case - easy to write
const props = { acceptCharset: 'utf-8' };
const { acceptCharset } = props;
// Kebab case - a bit more annoying
const props = { 'accept-charset': 'utf-8' };
const acceptCharset = props['accept-charset'];
The camel cased properties will work well with the simple key method, as that API expects the attribute name to be formatted the JavaScript way, for example element[“className”] = “my-class”
.
For names formatted the HTML way, we can use the element.setAttribute method, for example element.setAttribute(‘aria-hidden’, true)
.
Finally, to put them together, we can check whether the prop’s key would work for the key method, and if it doesn’t, switch to the setAttribute
method.
const applyPropToHTMLElement = ({ key, value }, element) => {
if (element[key] !== undefined) {
element[key] = value;
} else {
element.setAttribute(key, value);
}
};
Invalid keys for the key method, the element[key]
will be undefined
, while valid keys will always return a value different from undefined
, null or an empty string when they are not filled (well, at least on Chrome and Safari 😅).
We had originally used only element.setAttribute
(and it would in theory be possible to do so as long as we converted all names to the HTML ones), but we had discovered an interesting bug.
On Chrome, using element.setAttribute(‘value’, ‘’)
to reset an input element did not do anything, whereas element[‘value’] = ‘’
resets the input as one would expect.
One last thing to note is the complete solution, implemented in the next step, is handling one extra type of prop: event handlers.
We didn’t task you with this but we will need them later on in the App!
What about children?
HTML (and JSX) become much more powerful once they allow us to create a structured hierarchy of elements. In both cases, the elements that are contained within another element are referred to as its children
.
First let’s have a look at a HTML example, a div
with two children span
:
<div id="root">
<span>A first child</span>
<span>A second child</span>
</div>
and in JSX… the same thing
<div id="root">
<span>A first child</span>
<span>A second child</span>
</div>
On the other hand, our JS structure starts to be more complex to comprehend, as it involves nesting. For this reason we will start with a diagram before looking at the actual JS object.
On this diagram we can clearly see the nested hierarchy where the div is the root of a graph with 5 nodes.
That root is connected/contains the two spans, then each of those spans contains a text node.
And the children are added to the props object previously discussed.
Finally here is the JS object
{
type: 'div',
props: {
children: [{
type: 'span',
props: { children: 'A first child' }
}, {
type: 'span',
props: { children: 'A second child' }
}],
id: 'test'
}
}
An important thing to note here is what the children
of the span
look like.
In the JS object, if there is only one child for the children
property, instead of being an array with one item, it is just the child.
Equipped with this knowledge, we should be ready to render children!
If you are following along, it’s almost your time to attempt to implement the rendering of children.
If following along, it is now time to go to the first branch git checkout chapter-1/step-3
Before we leave you to implement, we must explain a few things we changed within the code provided in the dom-handlers
in the new branch. We have added more render functions 🤪 : renderElementToHtml
, renderTagElementToHtml
and renderPrimitiveToHtml
. Let us explain!
So far, you have only been tasked with rendering tag elements, but React also allows the rendering of primitive values like string
, number
or boolean
.
In the JS structure generated from the JSX, those elements are simply represented as is. For example, in the children example above, the string “A first child” ends up in the JSX as “A first child”.
To handle the rendering of primitives, we created the function renderPrimitiveToHtml
, to render them to their DOM equivalent:
const renderPrimitiveToHtml = value => {
switch (typeof value) {
case 'string':
case 'number':
return document.createTextNode(value);
case 'undefined':
return;
case 'boolean':
return value ? renderPrimitiveToHtml('true') : undefined;
default:
throw new Error(`Type ${value} is not a known renderable type.`);
}
};
It uses the type of the element we are trying to render to determine what to do.
For numbers and strings it uses the document.createTextNode API to render the value to a text node.
For undefined
it returns nothing, this will be useful later once we cover components which don’t render any UI.
For boolean
, it renders nothing for false
and “true” as a string for true
. This is useful for conditional rendering (i.e. {isPaul && ‘Hello Paul!’}
), where we expect falsey values should not render, but true statements should normally lead to regular UI being rendered.
Finally, for any other case, it throws an error as we do not know how to render them.
Then to tie both renderTagElementToHtml
and renderPrimitiveElementToHtml
together, we created the root function renderElementToHtml
:
const isPrimitiveElement = element => typeof element !== 'object';
const renderElementToHtml = element => {
const renderedElement = isPrimitiveElement(element)
? renderPrimitiveToHtml(element)
: renderTagElementToHtml(element);
return renderedElement;
};
It checks whether the element to be rendered is a primitive and uses one of the two rendering functions defined previously, and then returns the element that was rendered.
Now back to rendering children!
We have set up the code of the renderTagElementToHtml
function with a condition on children
props to render them there. To keep things simple at first, let’s think about how we would render just one child.
Children are JSX elements, and we have learnt how to transform JSX elements into DOM elements with the function we created: renderElementToHtml
.
So as a first step, if we do renderElementToHtml(child)
, we should get an HTMLElement that would be renderable to the DOM…
For now, this element is only held in memory in the JavaScript code, not in the browser’s DOM. We already had a similar problem in the past when we rendered the very first tag, can you remember which API you used?
Right! We used the appendChild API.
Now, we would like this child to be rendered within our current element, so we should be able to just call element.appendChild(childElement)
.
We have one last problem, in some cases for primitive values, renderElementToHtml
returns undefined
as the primitive should not be rendered to the DOM (that is the case for null
, undefined
and false
), so we must only append the child if it is a valid value.
Let’s put it all together:
if (children) {
// For now, let’s pretend we always have only one child
const child = children;
const childElement = renderElementToHtml(child);
if (childElement) {
domElement.appendChild(childElement);
}
}
Nice!
The last thing to consider is that children can be arrays, so need to handled appropriately, which could look like this:
if (children) {
// If it’s a solo child, let’s convert it to an array with one item
const childrenAsArray = Array.isArray(children) ?
children : [children];
// Transform all the JSX elements into HTML elements
const childrenAsDomElement = children.map(child =>
renderElementToHtml(child),
);
childrenAsDomElement.forEach(childElement => {
if (childElement) {
// Append each to our current element except if they don’t render anything
domElement.appendChild(childElement);
}
});
}
Great! Let’s now have a look at the implementation in the repository!
If following along, it is now time to go to the first branch git checkout chapter-1/step-4
You might realize we have organized things a bit differently compared to the code exposed here, most notably we have some of the rendering logic in my-own-react/index.js
(and we have added a bunch of VDOM code there 😱).
We have rearranged our code in order to separate our rendering between two concerns:
- The first one is what we have mostly dealt with so far, which is how to render elements into the browser’s DOM. This is very specific to rendering for the web, so we will keep this within
dom-handlers
. - The second one is dealing with React’s purposes: allowing components to be rendered, providing hooks for them etc. and is independent of the actual platform on which we render. We will keep this code in the
index
and refer to it as “React core” moving forward.
React’s code is organized in a similar fashion, where the main library react
provides high-level common UI rendering patterns and is rendering engine agnostic. It then has separate libraries, relying on the core library, to render to specific engines.
react-dom
is the most popular one to render to the browser, react-native
is another popular one that renders to Android/iOS views.
The VDOM parts will be the subject of our next article, so for now you can ignore those.
Now that we know how to render children, we are very close to being able to render any React code statically… our last step is to render components!
What about components?
A major difference between HTML and JSX is the fact JSX enables us to write components. In recent versions of React, most components are written as functional components, meaning our components are all JavaScript functions.
For our version of React, we will only implement the code for rendering functional components.
The signature of a functional component is to receive props as arguments and output something that is renderable: JSX!
Let’s look at an example component:
const MainTitle = ({ title }) => {
return <h1 className="main-title">{title}</h1>;
};
We would use such a component in JSX in the following way:
<MainTitle title="Build your own (simple) React!" />
And this would be transformed to the following JS structure:
{
type: MainTitle,
props: { title: 'Build your own (simple) React!' }
}
where the MainTitle
type is the function we defined as our component.
Now that’s a bit more mysterious than what we have seen so far, as a function is not something we exactly know how to render…
So let’s think about the problem again, we have a function component, which expects props as arguments and outputs JSX. And in the JSX we receive, we have as our type that function component and the props in an object…
And in the end, we would like to have the JSX rendered by the component.
If you want to scratch your brain a bit, you could start here and try to implement the component rendering in my-own-react/index.js
.
Ignoring the bits about the VDOM, your task is to return the “rendered” (as in, a JSX structure that the DOM handlers will know how to render to the DOM) version of the component.
If you need a little more help to get started, read along!
So with the contract described above: props in, JSX out, we can render our component to its JSX structure by calling its function (the type) with its props.
Following from our example above:
const componentJsxElement = <MainTitle title="Build your own (simple) React!" />;
console.log(componentJsxElement);
/* logs
{
type: MainTitle,
props: { title: 'Build your own (simple) React!' }
}
*/
const FunctionalComponent = componentJsxElement.type;
const componentChildren = FunctionalComponent(componentJsxElement.props);
console.log(componentChildren);
/* logs
{
type: 'h1',
props: { children: 'Build your own (simple) React!', className: 'main-title' }
}
*/
Now we know how to render the second part of this, as it is all things we have rendered before: a tag, props, children and a text node!
So the full implementation for the component rendering looks like this:
if (typeof type === 'function') {
const FunctionalComponent = type;
const renderedElement = FunctionalComponent({children, ...props});
const renderedElementDOM = render(
renderedElement,
// You can ignore those two lines for now, more to come in the next article!
VDOM,
[...VDOMPointer, 0],
);
return renderedElementDOM;
}
In a nutshell, we call the component with its props to get the JSX it is trying to render.
We then render this JSX to transform it into a structure that our DOMHandlers
know how to render.
Finally our DOMHandlers
will transform this JSX-like structure into actual browser’s DOM elements and append them to the DOM.
That’s a wrap 🎁 for this first article!
Congratulations on following along! 👏
What you achieved is already amazing!
We have a first version of a react-like library that can render (almost) any React application statically to the browser’s DOM.
But a To-Do app where we can’t add or remove to-dos isn’t the greatest… and in general, a Single Page Application only becomes powerful once it can provide some kind of state management…That’s the third article in our series!
But before getting there, we will look into the mystery of the holy Virtual DOM to set ourselves up for success.
Hope to see you there to continue on our journey into reimplementing React!
This article was written as a trio in collaboration with Ömer Gülen and Sean Blundell.