loading...

Creating your own "React", but only output DOM elements

merri profile image Vesa Piittinen ・8 min read

Hello! First post here :)

React has been out for good 5+ years by now and for the most part it is good. It does a lot to control human behavior into good habits when developing stuff. And all the dev tools, hot reloading and so on are great for projects with multiple members of varying levels of talent and experience.

It isn't all good though. By nature of virtual DOM there is a lot of extra work that will always happen under-the-hood that cannot be removed easily. This point is brought up pretty well by Svelte, which lets compiler do the work to optimize things for you. This allows for better performing sites when app grows into certain size.

But back on React's good parts. I like JSX. It does often result into quite readable code. What if we stole this into something of our own?

Having a look at what is out there

This isn't a new idea of course! JavaScript community is huge these days. Everything is invented in many ways and many times over. However, making something that actually solves a problem is quite a challenge, and it is even bigger if you can get to the level where everything that that is put together has synergy with each other part. So lets have a look into what we can find!

There is nativejsx that transforms JSX to direct native DOM methods, but it has a few notable downsides. First of all it requires extra client side JS that extends prototypes of HTMLElements. Giving new features to all DOM elements is a bit much. The worse part though is that the transpiled code is very verbose as it repeats document.createElement and others an awful lot of times, resulting in large JS which does compress well, but it is still a lot for browser to parse through. This has performance penalty. We probably rather want to output as compact syntax as possible.

Then there is jsx-dom that outputs DOM nodes directly. Sounds to be very close to what we want! This project emulates a lot of React with it's own implementation of things like createRef. The downside however is that the output is static: once you're done with building your DOM tree there are no further renders possible. How could this be solved?

The problem

The bad news is that there is no good way to output DOM nodes from JSX so that you would be able to call "render" again and only have changes happening. If you wrap anything in-between you're essentially re-implementing virtual DOM, and as our challenge we want to avoid that. We want DOM nodes out. But we also want them to update.

To highlight the issue with code, consider the following case:

function MyComponent(props) {
    return (
        <div>
            {props.visible ? 'You can see me!' : 'Nope'}
        </div>
    )
}

// React throws away other DOM elements and creates a new one for us
ReactDOM.render(<MyComponent visible={true} />, document.body)
// React does not expose DOM elements so we have to "magically" find it
document.body.querySelector('div').style.backgroundColor = 'black'
// React finds DOM node it owns and updates it
ReactDOM.render(<MyComponent visible={false} />, document.body)

We end up with page that has black div with the text "Nope". This is possible because under-the-hood React's diffing notices that we are still working with a div and re-uses the DOM node that is already on the page. And since React hasn't been told anything about the style attribute it doesn't pay any attention into it. This is why you end up with a black div with Nope.

So what will happen with a naive DOM implementation? MyComponent will output a DOM node. Then render clears document.body of any non-React children, and adds MyComponent's result there instead.

Upon next step non-React code kicks in and mutates the DOM node, setting the background color to black. So far so good!

But then we hit into a problem: we call MyComponent a second time and now we have two DOM nodes already: one that we created previously and the new one. If we go ahead and simply replace the old DOM node then our mutation is lost: the new div won't be black.

One could think: well, let's just diff the DOM elements! Then you have a look at what you need to do to: you'd need to have a complete list of every valid attribute, property and check all active bound events, too. Then there is the performance consideration: doing all that work is heavy already in how much stuff DOM nodes have. But the real killer here is that changing DOM elements is slow. In a simple case like the above it wouldn't matter, but with an app with hundreds of elements you'd soon be killing the battery of any mobile phone extra fast.

Breaking with React compatibility

To solve this issue we have to make something that allows us to update render as we go. Luckily there is one thing that allows for this: good old function!

function MyComponent(props) {
    // unlike React the <div /> and code before return is executed only once
    return (
        <div>
            {() => props.visible ? 'You can see me!' : 'Nope'}
        </div>
    )
}

// we want to keep only one DOM node
const App = <MyComponent visible={true} />
// add it to body (oh, we don't clear unknown nodes)
document.body.appendChild(App)
// mutation!
App.style.backgroundColor = 'black'
// render again... using a plain object
render(App, { visible: false })

So, in the example above we have updated MyComponent so that conditional code is executed within a function. We can track DOM elements and their related sub-functions so that we can call updates as necessary - and no more DOM elements are created. Well, except if the function itself outputs DOM element, but we'll get back to that a bit later.

In the code above, for it to work, the render method would also need to have a reference of original props in addition to the App DOM element. This would then allow to use Object.assign() to mutate the original props. But wait! Mutation is evil! It does have a tendency to result into unexpected bugs sooner or later.

One way to fix this particular issue would be to pass the props directly to the functions:

function MyComponent(props) {
    return (
        <div>
            {props => props.visible ? 'You can see me!' : 'Nope'}
        </div>
    )
}

But then we would have two truths of props: the original initial props and then ones that render passes on. There is also another issue: if we have another component within condition then we would be forced to create a new DOM element on each render and that is bad, because if we replace a node then all existing DOM state is lost.

Managing those nodes

So we need to have a way to manage visibility in a different way. The main limitation is that we can't do naive condition if the output is a DOM node. Something in the middle needs to take care of caching results.

Would there be something existing in React but that could be used for alternate purposes?

function MyComponent(props) {
    return (
        <div>
            <Fragment if={() => props.visible}>
                <span>You can see me!</span>
            </Fragment>
            <Fragment if={() => !props.visible}>
                <span>Nope</span>
            </Fragment>
        </div>
    )
}

Oh, meet Fragments. In DOM fragments are special in that they can't exist in the DOM tree. They're always top level parents, they can't be a child. If you render a fragment to DOM then only it's children will go there and the fragment becomes empty.

In our needs we can add a conditionality to fragments: when result of if's function is truthy, we can let the children be rendered to Fragment's parent. Otherwise we can capture them back to the fragment, if we like.

This allows us to keep cached copies of results so that when a re-render happens we simply return reference to existing DOM element that we have instead of generating a new one. Only time things go a bit worse (compared to React's virtual DOM) is when condition swaps: this is when we are forced to introduce a new DOM node. React's diffing can simply see a span DOM element and update only it's text.

The problem with Fragment is that we do end up with much more verbose syntax compared to React. At least in this case. We could go for shorter component name but then it would be like <If truthy={() => ...}> and I'm not so sure if that would be good. It might also encourage to implement components that would be conditional to components that come before, like <ElseIf /> and <Else /> and that would be a new kind of complexity as render result of a component would be tied to an otherwise unrelated component.

Things done to achieve this idea

Four years ago I wrote Nom: it only had a goal of outputting native DOM elements and most of my focus went to just getting it to work with diffing and updating, and having short syntax - and very large browser support ranging from as far back as IE5 times. Yikes. Less thought went into managing state and how to make it actually easy to use.

So recently I got back into the project and started modernizing it with all the experience and thoughts I've gained while working with React the past five years. It quite much makes sense to simply drop a whole lot of backwards compatibility and maybe only make things work with native ES6, so time would be spend into actually making something great that looks and works great in the future.

This got me into considering JSX and also the big issue that I hadn't tackled before: state management. A lot of React is about how to manage the state and when and how you can change it. There are recent additions like Hooks that make functions much more viable than they used to be.

But I've gone and improved NomJS. It doesn't work exactly like I've talked about earlier in this post: it relies on continuous updating using requestAnimationFrame and as such does not expose a render. It still also adds stuff to DOM nodes that it creates, a thing I don't like and want to remove. But for the most part it already works. If interested at Codepen you can find a demo app that tests various features. Or take a look at the source at GitHub. At the moment the code is in need of a major overhaul as I'm planning to remove all the Object.defineProperty stuff and instead rely on Map to keep track of DOM nodes created by Nom.

For now NomJS is in work-in-progress alpha status. Anything can still change as there are more special cases to be taken into account and more ideas to be had: like during writing this post I got a few new ideas like the truthy prop for If which didn't occur to me earlier. Also, state flow really needs more thought: currently mutation is very much king (as can be seen in the demo). And creating a render method instead of requestAnimationFrame does make sense. Giving option for both might be nice. Then there are things like lifecycle that remain unanswered: things like mounted(), updated(), unmounting(), unmounted() or whatever the naming convention could be.

I hope this sparks some thought for your brain. I left a lot of things unexplained, like I assumed you know how JSX works, but I hope that isn't too bad :)

Posted on Jun 6 by:

merri profile

Vesa Piittinen

@merri

Working with all things Front End, trying to account for the core of the web (HTML+CSS+SEO+A11Y) in the world of JS (mostly React). Dislikes when DX is put before UX. Enemy of div disease.

Discussion

markdown guide