Giancarlo Buomprisco

Giancarlo Buomprisco

ยท9 min read

Building Widgets with Solid.js Web Components

Solid.js is a small, familiar and super-fast library for building User Interfaces. In this post, we build a lightweight third-party widget as a Web Component using Solid and TailwindCSS.

SolidJS is a small library for building User Interfaces. It compiles JSX into highly optimized and efficient Javascript code: fast, small, and reactive. Its creator is Ryan Carniato, a brilliant developer at Netlify and previous at EBay.

Being a JSX-based UI library, it's naturally very similar to React; nonetheless, it has some peculiar differences and, more importantly, exciting strengths over React.

This blog post explains why Solid.js is a good choice for building third-party widgets and how I set it up for maximum developer experience so that you can do the same. Additionally, we'll leverage Tailwind CSS using a couple of workarounds.

NB: this is not an introduction to learning Solid - it's an explanation of how to use it for building third-party widgets; don't worry: if you know JSX, it's all you need to understand the contents of this blog post.

Why Solid.js?

Solid doesn't have the backing of a vast corporation and does not have a community as large as React's. So, you ask, why choose Solid over rock-solid libraries such as React?

It comes down not only to personal preferences but also to what is the goal you're trying to achieve. In this post, I won't go into detail about choosing one over the other. Still, I will explain why choosing Solid.js for building Javascript widgets is the better choice.

What is a Javascript widget?

Typically, any Javascript code that web pages inject from a third party. For example, a Chatbot widget, a popup, a calendar widget, etc.

These scripts are one of the main issues affecting modern web pages. Plenty of eCommerce businesses have dozens of widgets, and each of them downloads hundreds of KBs; in some cases, they bring whole frameworks, such as React (Ehm Ehm, isn't that right, Typeform?) with them.

It doesn't take a genius to understand that this is incredibly detrimental to certain businesses' revenue, especially because much of the traffic comes from mobile devices.

Why is Solid.js ideal for writing Javascript widgets?

If you're building a JavaScript widget, you want to ensure it minimally impacts your users' websites. And that's why Solid is an incredible choice for this use case: it is one of the fastest libraries out there, but it is also one of the smallest ones.

The best part? No compromise on your DX (developer experience): Solid supports Typescript, hooks, Suspense-like features, SSR, SSG, and you can easily bundle it with modern tools (such as Rollup).

It's everything you love about React, but smaller and faster.

Compiling Solid.js components to Web Components

Additionally, Solid.js allows compiling Solid components to Web Components.

What does it mean? We can use all the goodies of the Solid compiler and bundle our code as standard and compliant Web Components, reusable from other frameworks and libraries as standard HTML components.

To do so, we will use the solid-element library, built by the Solid.js team.

Building Widgets with Solid.js - a Crypto Price ticker widget

If I have convinced you to give Solid.js a try for building your widgets, read on. We're getting to the practical part of this blog post.

By the end of the post, we will have built a price ticker widget that displays the selected cryptocurrency price using the Coincap.io API.

Sounds fun? Let's go!

Installing the dependencies

First of all, let's create an empty folder, and initialize it with a basic package.json. Fire up your terminal, and run the commands (in order):

mkdir solidjs-web-component-widget
cd solidjs-web-component-widget
touch package.json

Now we can install the dependencies to be able to build our widgets.

Below are the packages I recommend installing using the following command:

npm i --save solid-js solid-element rollup babel-preset-solid rollup-plugin-terser
 rollup-plugin-babel rollup-plugin-postcss autoprefixer tailwindcss @babel/preset-typescript
 @rollup/plugin-node-resolve @rollup/plugin-replace

The above include:

  • Solid, Solid Element, and Solid's Babel preset
  • Rollup and the plugins to bundle our code (Babel, PostCSS)
  • Tailwind CSS, autoprefixer

Writing our first Solid.js Component

As you may know, writing Solid.js components is similar to writing React components (with a few, significant differences).

First, let's create our component PriceTicker at src/components/PriceTicker.tsx and some content as a placeholder:

function PriceTicker() {
  return (
    <div>
      Hello World
    </div>
  );
}

export default PriceTicker;

Congrats! We have written a valid Solid.js component.

Creating a service to fetch the ticker's prices

Let's proceed with creating a small service to fetch cryptocurrency prices from Coincap:

const BASE_URL = `wss://ws.coincap.io/prices?assets=`;

namespace CoincapService {
  export function createPricesStreamSocket(assets: AssetName[]) {
    const endpoint = [BASE_URL, assets.toString()].join('');

    return new WebSocket(endpoint);
  }

  export type Prices = Record<AssetName, number>;
  export type AssetName = string;
}

export default CoincapService;

The above really is a dead-simple service to fetch prices from the WebSocket. The prices have the following interface:

{
   [assetName: string]: number;
}

Creating an effect to stream prices

And now, we can create an effect, something akin to a React hook which fetches prices from the WebSocket, and updates a signal:

import CoincapService from "./coincap.service";
import { createSignal, onCleanup } from "solid-js";

export function usePrices(assets: string[]) {
  const [prices, setPrices] = createSignal<CoincapService.Prices>({});
  const socket = CoincapService.createPricesStreamSocket(assets);

  socket.onmessage = (message) => {
    const data = JSON.parse(message.data);
    setPrices(data as CoincapService.Prices);
  };

  onCleanup(() => socket.close());

  return prices;
}

Let's recap the above:

  1. We create a signal for storing the current price; in React, we could use useState:
const [prices, setPrices] = createSignal<CoincapService.Prices>({});
  1. Then, we initialize the socket and listen to each message. On arrival, we store the prices with setPrices:
const socket = CoincapService.createPricesStreamSocket(assets);

socket.onmessage = (message) => {
  const data = JSON.parse(message.data);
  setPrices(data as CoincapService.Prices);
};
  1. Using the onCleanup hook, we can close the socket when the component is unmounted:
const socket = CoincapService.createPricesStreamSocket(assets);

socket.onmessage = (message) => {
  const data = JSON.parse(message.data);
  setPrices(data as CoincapService.Prices);
};
  1. We simply return the prices that the component will display.

Displaying prices in our Widget

It's time to display the prices in our widget. To do so, we import the usePrices effect we created:

import { usePrices } from "../lib/prices/price-effects";
import { createMemo } from "solid-js";

interface Props {
  asset: string;
}

function PriceTicker(props: Props) {
  const prices = usePrices([props.asset]);

  const price = createMemo(() => {
    return prices()[props.asset];
  });

  return (
    <div>
      <p>
        <span>
          {price()}
        </span>
      </p>
    </div>
  );
}

export default PriceTicker;
  1. First, we call usePrices with a single asset (for simplicity):
const prices = usePrices([props.asset]);
  1. Secondly, we fetch the specific price for the asset specified in our component's asset property:
const price = createMemo(() => {
  return prices()[props.asset];
});
  1. Finally, we display the price. NB: we have to call price, because it's a function!

This is a pretty significant difference between Solid and React: in React, the whole component re-executes the function block. In Solid, it's only the render function that gets re-executed.

And that's it! We have prices streaming.

It's basic, but hey, it works!

Using Tailwind CSS with Solid's Web Components

Now that the functionality is nearly complete, we want to make it look better . To do so, we can use Tailwind CSS. To configure Tailwind, wait until we get to write our Rollup configuration.

Then, we create a CSS file named PriceTicker.css and we import it from the component PriceTicker.tsx; also, we create a style tag, and we inject the style:

.PriceTicker {
   @apply p-4;
}
import styles from './PriceTicker.css';

// component code
return (
  <>
    <style>{styles}</style>

    <div class={'PriceTicker'}>
      <p>
        <span>
          {price()}
        </span>
      </p>
    </div>
  </>
);

๐ŸŽ‰ Finally, we can now write Tailwind CSS with our Solid Web components. Yay!

Converting a Solid.js component to a Web Component

Before bundling our code, we want to convert a Solid component into a Web Component, which we can then call from a simple HTML file.

We create a register.tsx file with the following content:

import { customElement } from 'solid-element';
import PriceTicker from "./components/PriceTicker";

export function registerWebComponents() {
  customElement('price-ticker', getInitialProps(), PriceTicker);
}

function getInitialProps() {
  return {
    asset: ''
  };
}

Then, we import the function we defined above in our entrypoint file, which we call index.tsx:

import { registerWebComponents } from './register';
// add some global styles here
import './index.css';

registerWebComponents();

Bundling a Web Component with Rollup

To put everything together, we have to bundle our entry-point. To do so, we use Rollup.

Create a file rollup.config.mjs in the root folder, and copy the content below:

import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';
import babel from 'rollup-plugin-babel';
import postcss from 'rollup-plugin-postcss';
import autoprefixer from 'autoprefixer';
import tailwindcss from 'tailwindcss';

const extensions = ['.ts', '.tsx'];

export default {
    input: './src/index.tsx',
    output: {
        file: 'dist/widget.mjs',
        format: 'es',
    },
    external: [],
    plugins: [
        replace({
            'process.env.NODE_ENV': JSON.stringify('production'),
        }),
        resolve({ extensions }),
        babel({
            exclude: 'node_modules/**',
            presets: ["solid", "@babel/preset-typescript"],
            extensions,
        }),
        postcss({
            plugins: [autoprefixer(), tailwindcss({
                content: ["./src/**/*.tsx"],
            })],
            extract: false,
            modules: false,
            autoModules: false,
            minimize: true,
            inject: false,
        }),
        terser({ output: { comments: false } }),
    ],
};

To bundle our code, we add an npm script to the package.json:

{
    "scripts": {
        "bundle:widget": "rollup --config rollup.config.mjs"
    }
}

You can now bundle your Web Component by running the following command:

npm run bundle:widget

After running the command, you can find the compiled code at dist/widget .mjs; of course, rename it as you prefer using the Rollup configuration.

How big is our Web Component bundle?

After gzipping, our component weights only 3.9K. Lovely.

Using a Web Components with React.js

If you want to use a Solid Web Component from your React codebase, you can dynamically import a component:

import { useEffect, useState } from 'react';

const Widget: React.FC<{
  data: WidgetProp;
}> = ({ data }) => {
  const [, setLoaded] = useState(false);
  const id = performance.now.toString();

  useEffect(() => {
    (async () => {
      // @ts-ignore
      import('~/path/to/compiled/widget.js');
      setLoaded(true);
    })();
  }, []);

  return <my-widget key={id} data={JSON.stringify(data)} />;
};

export default Widget;

Now, import the component above anywhere, and use it as a standard React Component. Well, it is a standard React component, after all!

Using a Web Component Widget using a script tag

Now that we have bundled our component, we can import it from an HTML file and use it as a normal HTML element:

<html>
    <head>
        <script src="dist/widget.mjs" type="module"></script>
    </head>

    <body>
        <price-ticker asset="bitcoin"></price-ticker>
    </body>
</html>

And that's it. Our Web Component widget is ready!

Deploy the widget to any CDN (Firebase, CloudFlare, Vercel, etc.), and then import it on your web pages.

Final Words

In this blog post, we have learned how to leverage Solid.js to create small and fast Web Components.

Because of the small size, speed, and DX (Developer Experience) that Solid.js unlocks, building third-party widgets is a natural fit for this small yet powerful library.

I hope you enjoyed the article. If you need any help or have any questions, please reach out! Ciao!

NB: a Github repository with the source code will be added to this article very soon.