Intro
In this article, we will explore what custom renderers are, the problem they solve, and how SolidJS has created a custom renderer that is quite unique because of its simplicity and ease of use.
We will also learn how to create a custom renderer in SolidJS, and how to use it in our applications. Custom renderers have a long history with all the major frameworks in the market like Renderer2
in Angular, createRenderer
in Vue, and React Reconciler in React.
They are used to build awesome technologies and mind-blowing libraries. SolidJS recently added the ability to create a custom renderer with Solid Universal Renderer.
Note: We also have a great article about How to create a custom renderer for React that you can check out!
Custom Renderers in SolidJS
To better understand how custom renderers in SolidJS work, we have to understand it first in React and Vue (VDOM frameworks). In React, the library itself (React.js) doesn’t know anything about the DOM. It doesn’t understand what a div or h1 means. React converts all of the components into a JavaScript object that represents the UI in the memory (Virtual DOM), and then update the related nodes when the state changes in specific components.
That’s it! This is how React and Vue work under the hood. The React Renderer (which is a different package called “react-dom”) then works on rendering this Virtual DOM on the screen, and updates it when it receives updates from React.js. The same thing happens in Vue.
From here, the custom renderers have come to live. You aren’t limited to using only the official Renderer for the framework, but you can also build your own.. We will learn more about them in the proceeding sections. But for now, you just need to know that there is no virtual DOM in SolidJS. But it modifies directly on the DOM. This is the only difference between the Custom Renderers in SolidJS and React or Vue.
Real-world Custom Renderers
In this section, I’ll show you the most popular custom renderers that are used in many production applications in different frameworks. One of the most popular custom renderers in the industry is React Native (yes, it’s a React custom renderer for Android and iOS platforms). React-three-fiber is one of the most famous examples of React Reconciler in production. It converts the JSX VDOM to WebGL graphics. React-pdf is a React renderer for creating PDF files on the browser and server. TroisJS is a Vue custom renderer for Three.js and WebGL. There are many, many more examples.
HTML Nodes
Everything you see on the screen in the web browser is a node; the HTML element is a node, the attribute of that HTML element is a node, for example:
<img src="logo.png" /> // <- this node’s type is an element
// ^
// this attribute is a node
Also, the text between and opening and closing tags of this HTML element is a node:
<h1> // <- this h1 is type element
Hello world // <- this text is type text and it's a separate node
</h1>
Let’s code a SolidJS custom renderer
The process is going to be similar to React, but SolidJS Universal is even easier than React Reconciler because it has fewer options.
Create a new file to write our renderer and import the createRenderer
from SolidJS universal:
import { createRenderer } from 'solid-js/universal';
Second, let’s create an empty renderer:
const renderer = createRenderer({
// Add the renderer properties here
})
Renderer Options
Let’s break all of these options down in detail:
createElement
This option is responsible for handling the creation of a new element node. It takes only one parameter which is the element type and it could be something like h1
, p
, div
etc
createElement(type) {
return document.createElement(type);
}
And here we can play with the JSX elements for fun. For example, we can add our custom elements like:
function component() {
return (
<div>
<customLikeButton />
</div>
)
}
Technically, there is no HTML component called customLikeButton
. But here in this option, we can resolve this type as we desire.
createTextNode
This option is responsible for handling creating a new text node. It takes only one parameter which is the text itself.
createTextNode(text) {
return document.createTextNode(text);
}
replaceText
This option is responsible for handling updating the text node when it gets updates. It takes two parameters; the actual text node that has been updated, and the new text.
replaceText(node, text) {
node.data = text;
},
insertNode
This option is responsible for inserting or injecting a new node in the UI and the DOM. It takes three parameters, the parent node, the node itself, and a reference node (anchor) in the parent node.
insertNode(parent, node, anchor) {
parent.insertBefore(node, anchor);
}
removeNode
This option is responsible for removing a node from the DOM. It takes two parameters, the parent node, and the node which needs to be removed
removeNode(parent, node) {
parent.removeChild(node);
}
setProperty
This one is a little bit more complicated than the previous ones because it handles more categories. It is responsible for handling the attributes or properties of the element such as the classNames, style, and events like onClick and onSubmit.
setProperty(node, name, value) {
if (name === "style") Object.assign(node.style, value);
else if (name.startsWith("on")) node[name.toLowerCase()] = value;
else if (["classNames", "textContent"].has(name)) node[name] = value;
else node.setAttribute(name, value);
}
isTextNode
This option is used internally to determine if this node is a text node or not.
isTextNode(node) {
return node.type === 3;
}
Note: the
node.type
in the line above is a pure JavaScript code. In JavaScript nodes have 12 types with 12 numbers each representing a type. For example, 1 represents an element node, and 2 represents an attribute node. For more info see the HTML DOM Element nodeType on W3Schools and Node.nodeType - Web APIs | MDN.
getParentNode
This option is being used internally in SolidJS renderer to get the parent node of a given node.
getParentNode(node) {
return node.parentNode;
}
getFirstChild
This option is being used internally in SolidJS renderer to get the first node of a given parent node.
getFirstChild(node) {
return node.firstChild;
}
getNextSibling
This option is being used internally in SolidJS renderer to get the next node of a given node in the DOM tree.
getNextSibling(node) {
return node.nextSibling;
}
Renderer method
There are a lot of methods for this renderer, but the most important one is render
which we will replace the SolidJS web one with.
import renderer from "./renderer";
renderer.render(() => <App />, document.getElementById("root"));
There are additional methods available:
- effect
- memo
- createComponent
- createElement
- createTextNode
- insertNode
- insert
- spread
- setProp
- mergeProps
The performance of SolidJS Universal Renderer
Ryan Carniato, the creator of SolidJS, created a test on the SolidJS Universal Renderer in his stream of Benchmarking and Custom Renderers on his YouTube channel, and it was pretty interesting. It wasn’t much slower than the SolidJS web renderer at only a few milliseconds slower. To test it, you can use the js-framework-benchmark repo, which is an amazing tool you can use to compare the performance of different JavaScript frameworks.
Conclusion
In this article, we learned about the custom renderers in SolidJS and how to create one. We also learned about the performance of SolidJS Universal Renderer and how it compares to the SolidJS web renderer. I hope you enjoyed this article and learned something new. For more SolidJS resources, check out our solidjs.framework.dev where you can find all cool courses, tutorials, and libraries for SolidJS. Also you can create your next SolidJS project, try our SolidJS starter kit from Starter.dev it has a lot of tools pre-configured for you. If you have any questions or suggestions, please feel free to send them to us or reach out to us on Twitter.