SubtlefloSubtlefloSubtlefloSubtleflo

How to Create a Virtual DOM Without Using JSX or the h(createElement) Function

January 29, 2024 (1y ago)411 views

The author enjoys JSX and even created a JSX-based virtual DOM library called Lithent as a hobby.

While gathering information to create the virtual DOM library, the author came across the VanJS project. In VanJS, instead of using tag-definition styles like HTML or JSX to represent documents, the approach is to define them by calling functions, as shown below.

const Hello = () =>
  div(
    p('Hello'),
    ul(li('World'), li(a({ href: 'https://vanjs.org/' }, '🍦VanJS')))
  );

van.add(document.body, Hello());

Looking at this example, the readability doesn't seem too bad.

Upon reflection, I thought that one of the reasons people like JSX and why it’s popular might be because it's similar to traditional HTML syntax, which makes it familiar. However, for those who aren't as accustomed to markup languages like HTML, calling functions directly, like Tao Xin, the creator of VanJS, might feel more natural and complete.

After seeing that, I thought that if I were to work on a personal project, I'd like to try creating something similar.

The thing with JSX is that, in order for it to run in the browser, it's actually transformed during the build process into a format where h functions are called.

Comparing Readability with the h Function

Let's take the VanJS example I showed earlier and transform it into the h(createElement) style.

h(
  'div',
  null,
  h('p', null, 'Jello'),
  h(
    'ul',
    null,
    h('li', null, 'World'),
    h('li', null, h('a', { href: 'https://vanjs.org/' }, '🍦VanJS'))
  )
);

Tao Xin's approach seems to offer better readability, but both styles look quite similar. I thought it wouldn't be too difficult to apply Tao Xin's method to the virtual DOM library I made as a hobby. If the implementation isn't too hard, I think providing multiple expression styles wouldn't be a bad idea (since, realistically, I'll probably be the only one using this library in the future).

Let's go ahead and write the code.

Changing the h Function Style to the Function Call Style (Tao Xin's Style)

Using a Proxy as shown in the code example below, you can make it so that whenever a value is called from an object, it triggers the h function.

For example, when you run fTags.div(props, 'children'), it will call h('div', props, 'children') instead.

import { h } from 'lithent'; // 제가 만든 라이버리리의 h 함수

export const fTags = new Proxy(
  {},
  {
    get(_target, tagName: string) {
      return (props, ...childrens) =>
        h(tagName, props || {}, ...childrens);
    },
  }
);

In practice, it would be used like this:

const { section, div} = fTags;

section({ className: 'section'},
  div(null, 'div Block'),
),
// The actual DOM created will look like this:
// <section class="section">
//   <div>div Block</div>
// </section>

With the method described above, common tags like div, p, and section are all handled. But how can we transform something like a Fragment component or a user-defined component into a function call style?

I opened the editor and just started writing code, following the instructions of my head and hands.

Converting Fragment Component to Function Call Style

It was a bit daunting, but it turned out to be really simple. To handle a Fragment, you just need to create a function that takes children as a parameter and then call the h function to actually execute the Fragment component, like this:

export const fFragment = (...children) => {
  return h(Fragment, {}, ...children);
};

It would be used like this:

fFragment(
  section({ className: 'section' }, div(null, 'div Block')),
  p(null, 'p Block')
);
// <section class="section">
//   <div>div Block</div>
// </section>
// <p>p Block</p>

If you've followed along so far, you're probably thinking, "User components can be done in a similar way, right?"

Exactly. You can just do it the same way. In fact, reading further will likely lead you to the conclusion you've already anticipated. But since I've already decided to finish writing this post, I'll go ahead and complete it.

Converting User Components to Function Call Style

If you pass a user-defined functional component through the fMount function I implemented below, it will be transformed into a function call style component.

export const fMount = component => {
  return (props, ...children) => {
    return h(component, props, ...children);
  };
};

In practice, it would look like this:

const fUserComponent = fMount(function UserComponent(props) {
  return;
  fFragment(
    section({ className: 'section' }, div(null, 'div Block')),
    p(null, 'p Block')
  );
});
render(fUserComponent(), document.getElementById('root'));

// <section class="section">
//   <div>div Block</div>
// </section>
// <p>p Block</p>

Conclusion

I tried applying it to the virtual DOM I implemented (Litnet v1.9.0), and after finishing, I thought it could be easily applied to other JSX-based projects as well. In my subjective judgment, the readability isn't as bad as I initially expected.

By the way, the code I showed in this post is a simplified version for explanation purposes. You can find the original code here.

Since you've read all the way to the end, feel free to check out my lightweight virtual DOM library on GitHub and give it a star if you like!