Some times you have a problem that is best solved with a small footprint and naïve implementation of a UI library. But even if your requirements are simplicity and a few bytes, you can have declarative and functional UI components. In this post, we’ll see how we can use the JSX syntax with our own custom lightweight UI library.
While JSX was introduced by React, it is built on previous work in the language standard specification called E4X (ECMAScript for XML). The standard was later deprecated and is no longer supported. React isn’t the only library using JSX. Many others support it as well, such as Vue, preact, inferno, and more.
- Interchangeability with HTML, meaning we can more easily convert back and forth.
- Easier communication with designers who write HTML.
- Making the syntax more distinct, making it easier to visually separate data transformation and UI output.
These advantages are handy even if you don’t use large frameworks or libraries. We can write a small UI library that can utilize JSX to be more declarative, but still with a very minimal footprint. This will build on many of the same concepts as a previous blog post where we create our own library, but it uses a different approach and isn’t required reading to understand this post.
In practicality, this implementation will require a build step to transform our code, but we will only transform the JSX through Babel and use modern language features supported by all major modern browsers.
How does JSX work?
If we have a
package.json with the following Babel dependencies and configuration:
We can from our root (where the
package.json is located), build our
npm run build
With this setup, we can investigate what the output of JSX is. Given our
It will output:
Interesting! We see the tag is passed as a string to a function defined on the React namespace, and the last argument is the child, which is, in this case, a string of
Hello, World!. If we add some attributes (or props as it is often called with React), we can see what the second argument is:
The second argument is an object passed as properties to the
createElement function. How about child elements?
createElement is a variadic function where all but the first two parameters are children. We can say that the signature looks like this:
We can create a function that matches this signature and output our custom code. But for that to be possible, we somehow need to have the output of the JSX compilation be something other than
React.createElement. Great news! The Babel JSX plugin supports that.
We can use Babel plugin configuration to override the function:
Now our output would be:
Perfect! All we need to do now is create our own library.
Creating our own UI library
We know what signature we have to use:
tag is HTML tag,
props is an object of attributes, and
children is text content or other elements. Let's start with the first part, which is to create our DOM Elements with text content:
It works just as expected! We can also extend this to support other DOM Elements as children:
So let’s make a function
attrs which checks for what attributes we have and add them to the DOM Elements accordingly. For this example, we say we can have 3 different attributes: normal attributes, classes, and events.
With this we can enhance our elements:
And we should now have a working library with JSX support!
We are missing something really important, though. An important part of having declarative views is the ability to separate view blocks into different components. And JSX supports that. The first argument can be HTML tag string, or it can be a function, as illustrated by:
To support this we have to expand our
createElement function for special cases where our first parameter is a function. Before we do that, though, we have to look at how the signature is for custom components. In React it is normal to specify function components such as:
So it’s a different signature than our
createElement. The most notable difference is that the
children array is a part of the props, not as variadic rest parameters. Knowing this, we can extend our library:
And this will get us a long way! But if we look closely, there is a subtle bug when doing this. In our
Header component, we actually use
children directly as an array. Meaning, when we iterate through all children we try to append nested arrays as children to a DOM Element. To fix this, one way is to flatten out our children objects before iterating through it:
With some cleaning up and modularizing our final code looks like this: