• Reading time ~ 5 min
  • 06.07.2023

I'm personally a big fan of JSX and I love the way it allows me to split and link my code. Even though JSX existed before React, it wouldn't have been as popular if React hadn't raised it. However, we can actually use JSX without React, and that's not difficult either.  

React works by setting up its wrapper to convert JSX into createElement function calls. So, for example:However, most transpillers allow you to choose your own JSX pragma (a function that will replace ). For example, if you're using Babel, you can specify which function to use with a simple comment:

const foo = (
    <div className="cool">
        <p>Hello there!</p>
    </div>
)
// Would become this:
React.createElement(
    'div',
    { className: 'cool' },
    React.createElement('p', null, 'Hello there!')
)

  

/** @jsx myJsxFunction */
const foo = (
    <div className="cool">
        <p>Hello there!</p>
    </div>
)

And now Babel willReact.createElement pass some parameters myJsxFunction Now all we have to do is create a function that takes these parameters and create real DOM nodes that we can add to our DOM. So, let's get started. 

 DOM nodes are created using the document.createNode(), and it only requires a tag, so it's a good idea to start with this:

export const createElement = (tag, props, ...children) => {
    const element = document.createElement(tag)
    return element
}

Now that we have a DOM node, We have to actually add the attributes provided to us. It can be anything like class or style. So we'll just go through all the provided attributes (usingObject.entries  and just setting them on our DOM node):  

export const createElement = (tag, props, ...children) => {
    const element = document.createElement(tag)
    Object.entries(props || {}).forEach(([name, value]) => {
        element.setAttribute(name, value.toString())
    })
    return element
}

However, there is one problem with this approach.  For example, how will we handle events if I have this JSX:

const SayHello = (
    <div>
        <button onClick={() => console.log("hello there!")}>Say Hello</button>
    </div>
)

Our function will set onClick as a normal attribute with a callback as the actual text. Instead, we can check if our attribute starts with "on" and if it is in the scope of the window. This will show us if it is an event or not. For exampleonclick , in the scope of the window, however onfoo, no.    If so, then we can register an event handler on this node using the 'on' part of the name.

Here's what it looks like:Wonderful! Now all that's left to do is add all the child elements to the parent. However, you can't add a row to a DOM node, so in case the child element is also not a node, we can create a text node and add it instead:

export const createElement = (tag, props, ...children) => {
  const element = document.createElement(tag)
  Object.entries(props || {}).forEach(([name, value]) => {
      if (name.startsWith('on') && name.toLowerCase() in window)
          element.addEventListener(name.toLowerCase().substr(2), value)
      else element.setAttribute(name, value.toString())
  })
  return element
}

export const createElement = (tag, props, ...children) => {
    const element = document.createElement(tag)
    Object.entries(props || {}).forEach(([name, value]) => {
        if (name.startsWith('on') && name.toLowerCase() in window)
            element.addEventListener(name.toLowerCase().substr(2), value)
        else element.setAttribute(name, value.toString())
    })
    children.forEach(child => {
        element.appendChild(
            child.nodeType === undefined
                ? document.createTextNode(child.toString())
                : child
        )
    })
    return element
} 

However, this quickly leads to problems with deeply nested elements, as well as with elements that are created using array maps. So instead, let's replace this part with a recursive method: And now we can use this instead of our old method:It works! Give it a try. Now we can make a basic JSX for the DOM:

const appendChild = (parent, child) => {
  if (Array.isArray(child))
    child.forEach(nestedChild => appendChild(parent, nestedChild));
  else
    parent.appendChild(child.nodeType ? child : document.createTextNode(child));
};

  

export const createElement = (tag, props, ...children) => {
    const element = document.createElement(tag)
    Object.entries(props || {}).forEach(([name, value]) => {
        if (name.startsWith('on') && name.toLowerCase() in window)
            element.addEventListener(name.toLowerCase().substr(2), value)
        else element.setAttribute(name, value.toString())
    })
    children.forEach(child => {
          appendChild(element, child);
      });
    return element
}

import { createElement } from "./Vanilla"
/** @jsx createElement */
const App = (
    <div>
        <p>My awesome app :)</p>
    </div>
)
document.getElementById("root").appendChild(App)

And you should see your JSX rendered perfectly.  There are a few more things,appendChild that we can add, although, for example, in React, elements are usually functions whose implementation will allow us to nest components and take full advantage of the props that are the most important features of JSX.

К счастью, это довольно просто реализовать. Все, что нам нужно сделать, это проверить, является ли тег функцией, а не строкой. Если это так, мы не делаем ничего другого, а просто вызываем функцию. Here's what it looks like:Wonderful! Now all that's left to do is add all the child elements to the parent. However, you can't add a row to a DOM node, so in case the child element is also not a node, we can create a text node and add it instead:

export const createElement = (tag, props, ...children) => {
    if (typeof tag === "function") return tag(props, children)
    {...}
}

import { createElement } from "./Vanilla"
/** @jsx createElement */
const SayHello = props => (
    <div>
        <h3>Hello {props ? props.name : "world"}</h3>
        <p>I hope you're having a good day</p>
    </div>
)
/* <Component /> === Component() */
document.getElementById("root").appendChild(<SayHello name="foo" />)

/** @jsx createElement */
/** @jsxFrag createFragment */
const UsingFragment = () => (
    <div>
        <p>This is regular paragraph</p>
        <>
            <p>This is a paragraph in a fragment</p>
        </>
    </div>
)

Но чтобы это работало, нам нужна функция, которая берет этот фрагмент и вместо того, чтобы создавать элемент DOM, он просто возвращает его дочерний элемент. Here's what it looks like:Wonderful! Now all that's left to do is add all the child elements to the parent. However, you can't add a row to a DOM node, so in case the child element is also not a node, we can create a text node and add it instead:

const createFragment = (props, ...children) => {
  return children;
}

This works because of our recursive methodappendChild.

And that's it! We did it. A super simple JSX to DOM feature that allows us to harness the power of OSX without having to specifically use react. You can find the source code here.

  I hope you found this post helpful, and I also hope you find some cool ways to harness the full power of JSX. I actually learned all of this while working on Dhow, which is a static JSX site generator for Node.js. It basically allows you to write code in the style of Next.js, but converts it to static HTML without hydration issues.   Check it out and let me know, What do you think. 

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

ABOUT

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

About author CrazyBoy49z
WORK EXPERIENCE
Contact
Ukraine, Lutsk
+380979856297