From React to Web Components with Lit-HTML

The journey started when updating my portfolio site from react 16 to 18. After half an hour everything worked except for all the SVG imports. Two hours later I got frustrated and began to think utopia, as in minimising dependencies with web components…

Choices, choices

Native web component vs a utils library. Build system or not, typescript vs one Les dependency….. in the end I chose for: Lit-html, typescript and Vite for the build system.

My expectations/hopes are that in a couple years lit-html can be removed, typescript will remain and that I may need to look for another build system.

Choices in Lit-HTML

I made the choice to follow lit-html and declare the styles in the components with a single parent css file for the variables and baselines.
It is possible to render the components in the Light-dom with the cost of leaking CSS scope. With that downside in mind and that SEO seems too crawl shadow-dom I chose the latter.

The process

The process to convert a react component to a lit-html component was actually rather straightforward. Copy paste jsx, change props to @property, and change className for class. Especially className was so familiair for me that I forgot al lot. Web components with Lit-HTML have a lot in common with React before hooks. For example binding the this scope, to event handlers when using events.

this._handleChange = this._handleChange.bind(this);

Skill list in React

import React from 'react';
import { FiCheck } from 'react-icons/fi';

const SkillList = ({
    listData
})=> {
    const listItems = listData.map((item, index) => {
        return (
            <li className="SkilllListItem" key={index}>
                <FiCheck/>
                <span className="SkillListItemLabel">{item}</span>
            </li>
        )
      });

  return (
    <ul className="SkilllList">
      {listItems}
    </ul>
  )
}

export default SkillList;

Skill list in Lit-HTML

import { LitElement, html } from "lit";
import { map } from "lit/directives/map.js";
import { customElement, property } from "lit/decorators.js";

@customElement("portfolio-skills")
export class PortfolioSkills extends LitElement {
  @property()
  skills: string[] = [];

  protected render() {
    return html`
      <ul class="SkilllList">
        ${map(
          this.skills,
          (skill) => html`
            <li class="SkilllListItem">
              <svg
                stroke="currentColor"
                fill="none"
                stroke-width="2"
                viewBox="0 0 24 24"
                stroke-linecap="round"
                stroke-linejoin="round"
                height="1em"
                width="1em"
              >
                <polyline points="20 6 9 17 4 12"></polyline>
              </svg>
              <span class="SkillListItemLabel">${skill}</span>
            </li>
          `
        )}
      </ul>
    `;
  }
}

The webcomponent can than be used in html like. The . before skill makes it clear for lit-html that it is a property and not a default attribute .

<portfolio-skills 
    .skills="['React', 'HTML', 'Web Components']"
></portfolio-skill>

As can be seen above the resemblance between React and Lit-HTML is striking. I think Lit-HTML is a bit more verbose, hence that is a price I’m willing to pay since is (more) native, thus more robust in the long term.

Testing

Testing is workable, it is easy to test snapshots, properties, events and such. However the tooling (@open-wc/testing) feel a bit less mature than in the React ecosystem.

However when combined with reliable external libraries like sinon and other test helpers it should be possible to achieve the same coverage

Test example

import { html, fixture, expect } from "@open-wc/testing";

import { PortfolioSkills } from "../components/portfolio-grid/portfolio-skills";

describe("PortfolioSkills", () => {
  it("should display a list of 3 skills", async () => {
    const el: PortfolioSkills = await fixture(
      html`
        <portfolio-skills
          .skills="${["react", "Java", "Springboot"]}"
        ></portfolio-skills>
      `
    );

    expect(el).to.exist;
    expect(el.skills).to.have.length(3);
    expect(el.skills[2]).to.contain("Springboot");
  });
});

Thoughts

After decades in working in the frontend business I would be more than happy if web components became the defacto standard. I am aware of the fact that this could prevent fast innovation, since it will be locked into browser. Nevertheless, I think it the end it will help the industry especially for new developers if the tooling moved more slowly and durable.
learning frameworks becomes more easily if you have had to learn a few. The last few years I became a full stack Java developer with a focus on the frontend, and I came to appreciate the slower pace and the robust ecosystem of Java. Web components could be the start of the same robust and durable eco system for frontend.

The current code is still dependent on Lit-HTLML, TypeScript , both seem reliable enough to be there is a couple of years. Still for simple projects it would be nice to not depend on dependencies besides browsers.

CSS in build process

CSS vs SCSS, currently I have come to the point that the main advantage of SASS for me was splitting the files. Since CSS now provides a solution for variable. Another benefit of SASS is nesting style rules. Which absolutely is a benefit, hence not used carefully could lead to longer selectors with increased specifity.

So currently the ideal situation for me would be able to bundle several css files in to one css file with the right order. One possible option would be to use a cli option like cat on lunix

cat scr/css/styles.css scr/css/headings.css > dist/css/styles.css

Another option which which I prefer is to keep it in webpack like this, with the help of mini-css-extract-plugin (css-loader is also required).

Implementing

1. Install and css-loader and add mini-css-extract-plugin

run `npm install –save-dev css-loader mini-css-extract-plugin`

add `const MiniCssExtractPlugin = require(‘mini-css-extract-plugin’);` in the webpack.config.js

2. Add the styles in the entry section

entry: {
    ...
    styles: [
      path.resolve(__dirname, './src/css/styles.css'),
      path.resolve(__dirname, './src/css/headings.css'),
    ]
  },

3. add the rules for CSS

module: {
    rules: [
      ...
      {
        test: /\.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader'],
      },
      ...
    ],
  },

4. Always output chunks for the CSS

optimization: {
    splitChunks: {
      cacheGroups: {
        styles: {
          name: 'styles',
          test: /\.css$/,
          chunks: 'all',
          enforce: true,
        },
      },
    },
  },

5. Add the MiniCssExtractPlugin to the plugin section

plugins: [
    ...
    new MiniCssExtractPlugin({
      filename: '[name].css',
    }),
  ],

Wrap up

I like this approach, since standard CSS can be used which will improve and extend its responsibilities over time. It will never deprecate which is a huge bonus.
And it is flexible, I can make as many bundles as I wish and even decide to leave some CSS files out of the bundling process, for example to make use of HTTP2. A downside might be that you need to remember to manually add the the css files to the `webpack.config.js` file to get the styles.