DEV Community

Zend
Zend

Posted on

Build a Tiny React Ch1 JSX

Next Part

This series will build a tiny frontend framework, functionally similar to React, to illustrate how React works under the hood. This chapter covers JSX.

I will use Bun as runtime. Node may need extra configuration for typescript and compiling.

This tutorial is based on this tutorial, but with JSX, typescript and an easier approach to implement. You can checkout the notes and code on my GitHub repo.

JSX

Now, before we go any deeper, let's look at several important element of react- jsx first.

If you ever took a look at the transpiled code of a React component, you will see that it is just a bunch of function calls. JSX is just a syntactic sugar for React.createElement. That is, for exmaple, the following JSX code:

const element = <h1 className="greeting">Hello, world!</h1>;
Enter fullscreen mode Exit fullscreen mode

Will be transpiled to:

const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
);
Enter fullscreen mode Exit fullscreen mode

React.createElement will create a virtual element, which is another core mechanism. To put it simply, virtual element is the element in virtual DOM. A virtual DOM is something that represents the actual DOM. Since operating on virtual DOM is simply operating js objects, it is much faster than operating on actual DOM. We will talk about virtual DOM in the next chapter. But for now, knowing that JSX is just a syntax sugar for React.createElement is enough.

The React.createElement function takes the following arguments in order,

  1. The tag name of the element. Some tags are special, like div, span, h1, etc. These are called intrinsic elements. Some tags are user-defined components.
  2. The props of the element. This is an object that contains the properties of the element. For example, the className of the h1 element above is greeting.
  3. The children of the element. This can be string or element. Please note that this parameter is represented as ...children in the function signature, which means that it can take any number of arguments.

It sounds an easy job, right? So let's do it.

Implement JSX

When it comes to compilation, we can specify the function to use- by default, the function is React.createElement. But we can use our own function.

So we create a v-dom.ts, so as to define the virtual element first.

export type VDomAttributes = { 
    key?: string | number
    [_: string]: string | number | boolean | Function | undefined
}

export interface VDomElement {
  kind: 'element'
  tag: string
  children?: VDomNode[]
  props?: VDomAttributes
  key: string | number | undefined
}

export type VDomNode = 
| string
| VDomElement
Enter fullscreen mode Exit fullscreen mode

Please note that we have a key field in each node (node is just a name for either text or element). This is for reconciliation, which we will talk about in the next chapter. You can safely ignore it for now.

Now we can implement the createElement function. We put it in the same file.


export function createElement(tag: string, props: VDomAttributes, ...children: VDomNode[]): VDomElement {
  console.log('createElement', tag, props, children)
    return {
        kind: 'element',
        tag,
        children,
        props,
        key: props?.key ?? undefined
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we instruct our compiler to use this function. We can do this by adding the following line to the top of our file.

import { createElement as h } from './v-dom'
Enter fullscreen mode Exit fullscreen mode

Please note that since we are adopting react standard, we need to introduce the React type definition. We can do this by adding the following line to the top of our file.

bun i @types/react
Enter fullscreen mode Exit fullscreen mode

Then in the tsconfig.json, we add the following line to the compilerOptions field.

"compilerOptions": {
    "jsx": "react",
    "jsxFactory": "createElement",
}
Enter fullscreen mode Exit fullscreen mode

Now, we can take a look at the virtual element we created.

import { createElement } from "./v-dom";

function App() {
    return <div>
        <h1>a</h1>
        <h2>b</h2>
    </div>
}

console.log(App());
Enter fullscreen mode Exit fullscreen mode

You will see just our defined virtual dom element based on the JSX code.

Furthermore, we can also define a fragment element- the <></>, if you don't know its official name. We can do this by adding the following line to the top of our file.

When dealing with fragment, the compiler will take the configured fragment factory to the tag in the element creation function. This is the same as how functional components work- functional component will take the function to the tag, which we will demonstrate in the next chapter.

Nonetheless, in our implementation, there is no need for complex handling- we just need to set a special tag for fragment.

export type VDomAttributes = { 
    key?: string | number
    [_: string]: string | number | boolean | Function | undefined
}

export interface VDomElement {
  kind: 'element' | 'fragment'
  tag: string
  children?: VDomNode[]
  props?: VDomAttributes
  key: string | number | undefined
}

export type VDomNode = 
| string
| VDomElement

export function createElement(tag: string, props: VDomAttributes, ...children: VDomNode[]): VDomElement {
    console.log('createElement', tag, props, children);
    return {
        kind: tag === '' ? 'fragment' : 'element',
        tag,
        children,
        props: props ?? {},
        key: props?.key ?? undefined
    }
}

export const fragment = ''
Enter fullscreen mode Exit fullscreen mode

Extra compiler options,

"jsxFragmentFactory": "fragment"
Enter fullscreen mode Exit fullscreen mode

Basically, fragment is just a special element with an empty tag. When it comes to the creation of fragment, the compiler will fetch the jsxFragmentFactory and put it into the tag parameter in the first parameter of createElement. So we can easily distinguish fragment from other elements.

import { createElement, fragment } from "./v-dom";

function App() {
    return <>
        <h1>a</h1>
        <h2>b</h2>
    </>
}

console.log(App());
Enter fullscreen mode Exit fullscreen mode

This code will correct yield the virtual DOM. So far, we have implemented the JSX part of our tiny react.

Footnote

Ugh, this is the author from chapter three. Actually, the current implementation of JSX is not perfect. We will fix it in the third chapter. Now it does not support syntax like,

<div>
    {[1, 2, 3].map(i => <span>{i}</span>)}
</div>
Enter fullscreen mode Exit fullscreen mode

This is because every {} is treated as one child, where as the map returns an array. So it will have nested children.

Also we hadn't supported functional components yet, which is in the next chapter.

You can just follow the current one and fix it later. Sorry for the inconvenience.

Top comments (0)