Web Components + React Hooks = Haunted 🦇 🎃

Sergey Kondratenko
ING Blog

--

The React team introduced Hooks at React Conf 2018. Technically it might not have been a particularly big innovation, but it did change a lot in the approach for building UI components. It triggered a big wave of changes in many other open source projects and quite some new open source projects were created, like Redux Hooks, Apollo Hooks, TNG, etc. It even inspired Evan You (the creator of VueJS) to think about introducing Hooks in VueJS.

It’s no secret that ING works a lot with Web Components that are made using lit-html and LitElement — we even have an article about it. Initially, I was keen to build some kind of a support for Hooks in Web Components but in the Front End world if you do not bring your idea to life today, tomorrow you are too late. There is already a brilliant library which makes it possible to use Hooks together with Web Components. It’s name is Haunted.

I recently introduced this library in my team. Not everybody knew what Hooks are and what kind of benefits we can get from them. They were skeptical: it’s an extra dependency, there are no benefits for the user of the application, there are no performance improvements. Why would a developer use Hooks then?

I have managed to convince my team members, let’s see if I can convince you with this article.

We started using Haunted in two of our projects that initially were written using LitElement and our code became much cleaner. The code refactoring went smoothly as our applications are built with web components. It is not needed to begin from scratch, you can just rewrite it component by component. Moreover client side data in those projects is managed by Redux, so a huge part of application remained untouched. But the component classes and the logic around them became more compact and more readable. And at the end everyone in the team are happy with the result.

I will try to explain to you now in more details why the Hooks are so cool. But first, let’s introduce some key concepts.

lit-html

What is lit-html? As they say in the guide: "It is a simple, safe, small and fast HTML templating library for JavaScript". Lit-html uses JavaScript tagged template literals to create expressive, dynamic templates, and is made by the Polymer team at Google.

In this example, html is just a function that uses the string with the injections as input and makes a TemplateResult that can be rendered as DOM.

Usually you’ll have to provide the data to your template so you write your template as a function that can be called multiple times:

  1. Function myTemplate that accepts an argument with the values that have to be injected in template.
  2. Creation of TemplateResult with injected values.
  3. Rendering TemplateResult to document.body.

It is worth mentioning that this library is very small, and checks in at only 3.5kb. It is highly optimized for rendering and very efficient, without use of virtual DOM and rerenders only the parts that are necessary. The syntax looks very similar to React’s JSX templates, but the great thing about lit-html is that it uses plain javascript features without any transpilation magic behind the scenes.

LitElement

Lit-html is a render library and can be used without usage of web components. However, if you do want to develop web components with lit-html, you can use LitElement which is also made by the Polymer team from Google.

  1. These are the properties that trigger re-rendering and that can be specified as attributes in html tag, like: <my-el name="Everyone"></my-el>
  2. This is the render function that produces TemplateResult to be rendered to shadow dom

React Hooks

So after reading this, what are hooks? The intro page states the following: “They let you use state and other React features without writing a class”. Indeed now React components can be written using simple functions and not classes like the LitElement example above. Apart from state there are also hooks for component’s lifecycle, context, memoization for complex calculations, reducer and many more. You can also build your own custom hooks.

I am not going to talk about all types of hooks you can create, but I will try to explain the concept via the often used useState hook:

Here count is the state value, setCount is the function that allows you to change the count state value. The only argument in useState is the initial state, so in our case by default count is initialized with 0.

The MyComp function is called upon each render and it is up to framework to preserve state between renders. You can also think about this hook in the following way: if MyComp was a class then count would be the class field with the change listener for re-rendering.

If you want a detailed explanation of all the hooks you can find it here.

Haunted

Haunted is a library that provides React Hooks API for standard web components. It uses lit-html as templating engine by default but you can also use other template engines like lighterhtml or hyperHTML.

Haunted is all about writing plain functions instead of classes that can contain their own state using hooks. Here is the simplest hello world example:

As you can see instead of using classes as it is in LitElement you use plain functions here. If you ever worked with React, then you can notice that it looks very similar to their functional components.

LitElement vs Haunted

So lets see how LitElement compares to Haunted. The following web component will show whether your device is offline or online.

You can see above that we defined properties. We have a constructor where we initialized default values, and we have 2 lifecycle methods connectedCallback and disconnectedCallback to add/remove listeners. We also have render method for output.

  1. we define 2 properties: title of type Boolean and online of type Boolean
  2. we assign the default value taken from navigator
  3. we add an event listener for online and offline event
  4. we remove the event listener for online and offline event
  5. we render the output to shadow dom

Here is the same functionality with Haunted:

You can see that they are quite similar although the Haunted example has a bit less boilerplate and is written with a complete functional approach.

What are exactly the differences?

  1. We distinguish if a property is an html attribute differently. In Haunted the values can be injected from attributes only if they are mentioned in observedAttributes. So in our case title is also an html attribute andonline is only a state value which is managed by useState hook.
  2. The default state value in Haunted can be specified right in useState hook.
  3. Instead of using component lifecycle to add window listeners (like connectedCallback in LitElement). We can achieve the same with theuseEffect hook with cleanup. TheuseEffect hook is used to execute side effects so in the main body of useEffect function we add listeners.
  4. And in the returned ‘cleanup’ function we remove the listeners (like disconnectedCallback in LitElement).
  5. We just return our html template to render the output

Styling

CSS can be added in the most obvious way:

Although if you want to use the css helper function from LitElement that’s still possible. For this you would need to add the haunted-lit-element library to your project that adds LitElement features support for Haunted.

In that case, the previous example can be transformed to:

You can see above that styles can be injected in the component function as a second argument. Notice that we are now importing the component wrapper function from haunted-lit-element instead of importing it from Haunted.

Attribute and properties

The property and attribute reflection that comes with LitElement is also supported by Haunted in combination with haunted-lit-element:

As you can see you just have to inject the properties configuration object, built in the same way as you would do it for LitElement, as the second argument in component function.

And then for the following markup:

you will get the following output:

All the values are going to be converted to the right type with the help of LitElement.

Events

In web components you send a custom event using dispatchEvent method of the element instance. In Haunted you can get access to the element's instance using the this keyword:

Notice that in this case you cannot use arrow functions for your component definition otherwise the this keyword will not work as it should point to the context of the component function which is the element's instance.

Custom base class

Every web component extends HtmlElement. For example LitElement is also based on HtmlElement. If you use pure Haunted then all your components are based on HtmlElement and if you use haunted-lit-element then all your components are based on LitElement.

Sometimes you might want to specify your own base class. For example instead of using component instance while dispatching custom event you can create your own base class that binds the context to dispatchEvent function:

Then while creating the component you can provide your own base class as a second parameter:

After that you can just destructure your method as a function argument:

And finally our custom event should look a little bit more pure like so:

Advantages

If an obvious choice for your next project are Web Components, why would you choose to build it also using hooks-based framework?

Popularity of React

Well, first of all at the moment React is the most widely used javascript framework and one of the most starred in Github (rank 4 on the moment of writing the article). If you take “the big three” of front end frameworks: React, VueJS and Angular then probably the biggest part of Front End Engineers on the Market would be occupied by React developers. They are all used to work with functional components and hooks, and it will definitely help you to find the right professionals that can immediately start to bring the benefit. Check also this fair question on twitter asked by Sebastian Markbåge (one of the core React developers).

Classes vs Functions

In my opinion functions are much easier to follow. You just have one entrance, one exit and logic in between. At the end you also get less boilerplate code. You may have also noticed that Haunted components make your code much more concise.

Mixins vs custom hooks

The current default LitElement’s approach for sharing state between components is based on Mixins. React community already had problems with it in the past. The developers in Facebook have experienced that sharing the code between components using Mixins leads at the end to unmaintainable and confusing code. And react hooks were made exactly as one of the solutions for it.

Imagine that you want to have the same online functionality that we have built above in other component. Here is the Mixin that you would build for LitElement implementation:

What are the drawbacks with this approach? First of all if you look at the code of MyEl class, the connection between OnlineMixin and the online property is not immediately clear, you really have to know it.

And second, if you would need to use more mixins you might potentially have conflicts as all of them can create class instance fields and there is no guarantee that the field name is not already taken.

How would you move out the same functionality in our Haunted element?

As you can see, it couldn’t be simpler. You immediately see the connection, it is very easy to read the code, and there cannot be any conflicts. It doesn’t matter how many hooks you are going to use, as you define the names yourself.

Sharing Hooks

You can share your business logic (hooks) with the components written using a different hooks-based framework. In this case you have to build your hooks so that they can receive useState, useEffect etc. as parameters so that React or Haunted or any other hooks compatible library could be used:

And then you could use it in a React project like so:

And in a Haunted project you do:

Some code from this article is available in sandbox.

Questions are welcome, feel free to contact me on linkedin.

Thanks for reading!

I want to thank Richard van der Molen for helping me with this article.

--

--