Future-Proof Your Design System with StencilJS

An introduction to StencilJS: A compiler that generates web components.

Giancarlo Buomprisco
Bits and Pieces

--

Image by Angel-Kun from Pixabay

What is StencilJS?

StencilJS is a library for generating web components, built by the team behind the Ionic Framework.

As you may know, when Ionic was initially released, it was built specifically for Angular. Then, other frameworks started to emerge and take a large chunk of the frontend community — and Ionic wanted its tools to be used by anyone regardless of the framework of choice.

Rewriting the same components for every major framework would have been an impossible (and probably wrong) task. Web Components allow us to solve this issue — but not without limitations, that are all well-documented.

Stencil attempts to provide an abstraction on top of Web Components to simplify how we write and ship them.

One of the most interesting things about Stencil is that the code compiled is shipped without Stencil (just like Svelte, another “disappearing” framework): this allows the components to be incredibly lightweight and makes them ideal for being used with other frameworks such as React, Vue or Angular.

💡 It is for their lightweight nature and native browser support that Web Components are increasingly used for design systems. They can even be incorporated into existing modular React/Vue/Angular apps with open-source tools such as Bit.

Learn more here:

So, for example, you can push newly built Web Components to your Bit collection (library) that up until now has only contained React/Vue/Angular components (and let your team use them in your projects). Bit comes with a component development environment for Stencil that provides proper tools, configurations and runtime environment for your components.

Modular and dynamic design systems on bit.dev

Why I chose Stencil for my Project

I’ve recently embarked on a personal project that has taken much of my time lately.

When I had to make a choice about what tools to use, I started researching a library for building a collection of components with the following conditions:

  • It needed to be very fast
  • It needed to be very lightweight and simple
  • It needed to be extremely future-proof
  • It needed to be supported by an active and vibrant community

My use case is to build 3rd party widgets to be loaded on the user’s websites. As you can imagine, performance and weight are essential. No one wants a 3rd party to slow down their website!

Possible Candidates

Other than Stencil, I started researching various other tools:

While the above are extremely valid options, and probably would have worked also very well for my use case, I decided to go with Stencil for the following reasons:

  • JSX: love it or hate it, it is very well known and it is used in many other frameworks. Anyone could pick it up in a matter of hours.
  • Small and smart bundles: all components are lazy-loaded and will use the correct bundle for the browser being used thanks to differential loading
  • First-Class Typescript Support — this was pretty important to me
  • Despite using JSX as a templating language, as a primarily Angular user, I found it extremely easy to get started with and pretty much everything made sense from the beginning. There are some things to watch out as we will see in the next sections.

After having used Stencil with great delight, I decided to write this article to introduce you to this tool and share my experience.

The building blocks of StencilJS

In this section, we’ll see how to build one with Stencil, starting from the basics. To understand how Stencil works, we’ll explore its most important topics:

  • Defining a component
  • Passing properties
  • Handling internal state
  • Emitting Events
  • Exposing Methods
  • Templating with JSX and slots

Once you get a grasp of these concepts, you can be up and running with writing Stencil components in very little time! Yes, it’s that easy.

The Anatomy of a Stencil Component

A stencil component gets declared with the Component decorator; yes, this may be familiar, it does look like Angular.

We define:

  • its tag name with the property tag
  • the component’s styles using the property styleUrls
  • a function named render that is responsible for defining the template using JSX. And yes again, that’s familiar, because it works similarly to React’s class components.
// single-choice.tsximport { Component, h } from '@stencil/core';@Component({
tag: 'single-choice',
styleUrls: ["./single-choice.css"]
})
export class SingleChoiceComponent {
render() {
return 'I will be a single choice field!';
}
}

Notice: import h is needed if we use JSX within the render function

Once we build the component using the Stencil compiler and import the scripts on our web page, we can simply call the component as we would with a normal HTML element:

<single-choice></single-choice>

Scoped and Native Shadow DOM

Web Components can be scoped using Shadow DOM (using the property shadow), but this is still not supported across all browsers (such as IE11, and Safari only partially supports it).

Just like Angular’s emulated view encapsulation, Stencil provides a property called scoped which will emulate the same behavior and encapsulate the style of our components.

By default, I always set scoped instead of shadow.

@Component({
tag: 'single-choice',
scoped: true
})

Passing Properties to Components

Passing properties to components also remind a lot of how Angular works. In order to pass properties, we can define class properties and decorate them with the decorator Prop.

import { ..., Prop } from "@stencil/core";@Component({...})
export class SingleChoiceComponent {
@Prop() id: string;
render() {
return (
<label for={this.id}></label>
);
}
}

This decorator though can accept some configuration you may be unaware of:

  • attribute: the name of the attribute to pass, in case the class property’s name needs to be different
  • mutable: by default, properties are immutable. Once it’s set from outside of the component, it cannot be mutated — unless we explicitly set this property to true
  • reflect: if we want to expose the attribute to the DOM of the component, we can set this property, so that we can access the property from the outside
@Component({...})
export class SingleChoiceComponent {
@Prop({
attribute: 'id',
mutable: true,
reflect: true
})
fieldId: string;

render() {...}
}

Now that we exposed the property id, we can access it using the DOM API:

const singleChoice = document.querySelector('single-choice');
const id = singleChoice.id;

Internal State

Stencil attempts to maximize performance and efficiency by re-rendering only when necessary. If you’re used to a framework like Angular, or Svelte, you may not immediately understand why your component is not updating its view.

In order to trigger a re-render when a property changes, Stencil provides the decorator State:

@Component({...})
export class SingleChoiceComponent {
@Prop() id: string;
@State() value: string;
render() {...}
}

Notice: If we forget decorating the property value, the view will not re-render.

Events

Of course, components can also expose events to their parents to enable child-parent communication.

An event is defined in the following way:

  • we import the decorator Event and set it to its relative property
  • we define its type, which is an EventEmitter
@Component({...})
export class SingleChoiceComponent {
@Prop() id: string;
@State() value: string;
@Event() valueChanged: EventEmitter<Option>; onClick(option: Option) {
this.valueChanged.emit(option);
}
render() {...}
}

If you are using the event with a child rendered within the render function, you can simply pass the event down and call a method:

<parent-component>
<single-choice
valueChanged={(e) => this.choiceSelected(e))
></single-choice>;
</parent-component>

In cases where our child component is nested deep within the parent’s children, we can listen to custom events thanks to the decorator Listen.

class ParentComponent {
@Listen('valueChanged')
valueChanged(value) {
// do something with value
}
}

Methods

Component methods are not to be confused with the methods you normally define in the component’s class. Stencil allows certain methods to be exposed as public API by decorating them with the decorator Method.

@Component({...})
export class SingleChoiceComponent {
@Prop() id: string;
@State() value: string;
@Method()
async getValue() {
return this.value;
}
}

Once the method is defined, we can call it from the outside:

const singleChoice = document.querySelector('single-choice');
const value = singleChoice.getValue();

Gotchas:

  • Public methods should always be async
  • It’s not recommended to use public methods to expose the source of truth of a component. The suggestion is to rely solely on events and props instead.

Templating: JSX and slots

If you have ever worked with React, Preact or any other library that uses JSX, there’s not much new for you to learn to start using Stencil. If not, there’s a little bit to learn, but fortunately, JSX is fairly simple.

Of course, you can also define functional components and use them in the render function:

const Label = (_, text: JSX.Element) => 
<label>{text}</label>
@Component({...})
export class SingleChoiceComponent {
render() {
return (
<Label>
<slot />
</Label>
);
}
}

As you can see above, slots can be helpful to render the content of a component. You can also define named slots to control where the content will be rendered.

class MyComponent() {
render() {
return (
<div>
<slot name="heading">
</div>
);
}
}
<my-component>
<h1 slot="heading">Heading</h1>
</my-component>

Final words

StencilJS has been a great library to work with. As I work mostly with Angular, I was pretty used to having a really good developer experience and a rich ecosystem, but Stencil hasn’t let me down in this regard.

It is a very valid choice if you want to complement your applications with a set of highly reusable components. As mentioned earlier, you can gradually build a design system composed of Web Components or replace an existing one (implemented with some framework) by using tools like Bit. This will future-proof your design system and make it available to every other front-end technology used within your company. It’s also a valid choice if you’re using plain JavaScript, as the overhead added is extremely minimal.

Learn more here:

Sure, I’m hitting bugs here and there, but the team is generally very responsive and they ship new releases very often. Having Ionic’s components as a reference is also a great way to figure out best practices and see how the core team approached their architecture.

Expect more articles about Stencil from me in the future!

Resources

If you need any clarifications, or if you think something is unclear or wrong, do please leave a comment!

I hope you enjoyed this article! If you did, follow me on Medium, Twitter or my website for more articles about Software Development, Front End, RxJS, Typescript and more!

Build Apps with reusable components, just like Lego

Bit’s open-source tool help 250,000+ devs to build apps with components.

Turn any UI, feature, or page into a reusable component — and share it across your applications. It’s easier to collaborate and build faster.

Learn more

Split apps into components to make app development easier, and enjoy the best experience for the workflows you want:

Micro-Frontends

Design System

Code-Sharing and reuse

Monorepo

Learn more:

--

--