IALab - Et si on parlait le même langage : StencilJS

Pour notre projet de chatbot en marque blanche, nous souhaitions offrir au client final la possibilité de choisir la technologie frontend qu’il préfère. Ce choix doit non seulement permettre de s’adapter aux compétences des équipes du client, mais aussi tenir compte d’éventuelles contraintes techniques ou organisationnelles. Nous avons donc recherché une approche qui permette de développer notre frontend sur différents frameworks tout en facilitant sa maintenance.

Les différentes solutions envisagées

La première solution envisagée, et la plus basique, a été de développer un frontend par framework. Cette approche a l’avantage d’être simple en termes d’architecture et de pouvoir appliquer toutes les bonnes pratiques relatives à chaque framework. Cependant, cette solution est longue et fastidieuse en développement et en maintenance. Chaque nouvelle fonctionnalité ou correction doit être reportée sur les différents projets.

La deuxième idée a été de s’intéresser aux Web Components. Ce sont des standards natifs du web pour créer des éléments HTML personnalisés. Grâce à eux, nous pouvons encapsuler la logique et le style tout en restant compatibles partout. Des librairies existent pour faciliter leur création, comme Lit ou Slim.js. Cependant, l’utilisation de ces Web Components dans des projets basés sur des frameworks comme Angular ou React fait perdre toutes les facilités propres à ces frameworks pour manipuler les composants; Comme par exemple le binding, la gestion du state ou le cycle de vie automatisé. C’est là que StencilJS rentre en jeu.

Qu’est-ce que StencilJS ?

StencilJS est un compilateur open source développé par l’équipe Ionic. Son objectif principal est de créer des Web Components réutilisables, compatibles avec tous les frameworks frontend (React, Angular, Vue…) ou utilisables nativement sans framework.

Parmi ses fonctionnalités clés, on retrouve :
- Génération de composants légers et performants.
- Agnostique vis-à-vis des frameworks.
- Fournis des outils de base comme le router et le store.
- Support de TailwindCSS pour un design rapide et moderne.

Créer un Web Component avec StencilJS

Il est tout à fait possible de créer des Web Component nativement, en voici un exemple :

class MyCounter extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: "open" });
    this.count = 0;
    this.increment = this.increment.bind(this);
  }

  static get observedAttributes() { return ["start"]; }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === "start" && newValue !== oldValue) {
      this.count = parseInt(newValue, 10) || 0;
      this.render();
    }
  }

  connectedCallback() {
    this.count = parseInt(this.getAttribute("start"), 10) || 0;
    this.render();
  }

  increment() {
    this.count++;
    this.render();
    this.dispatchEvent(new CustomEvent("countChanged", { detail: { count: this.count } }));
  }

  render() {
    this.shadowRoot.innerHTML = `<button>Compteur : ${this.count}</button>`;
    this.shadowRoot.querySelector("button").onclick = this.increment;
  }
}

customElements.define("my-counter", MyCounter);

Les Web Components sont fonctionnelles, mais ils peuvent rapidement devenir verbeux et difficiles à maintenir pour des projets complexes.

StencilJS, lui, utilise TypeScript et JSX, avec une syntaxe proche de React. Il repose sur des décorateurs pour gérer les propriétés (@Prop), l’état (@State), les événements (@Event) et le composant lui-même (@Component).

Exemple simplifié avec StencilJS :

import { Component, Prop, State, Event, EventEmitter, h } from '@stencil/core';

@Component({
  tag: 'my-counter',
  shadow: true
})
export class MyCounter {
  @Prop() start: number = 0;
  @State() count: number;
  @Event() countChanged: EventEmitter<{ count: number }>;

  componentWillLoad() {
    this.count = this.start;
  }

  private increment = () => {
    this.count++;
    this.countChanged.emit({ count: this.count });
  };

  render() {
    return <button onClick={this.increment}>Compteur : {this.count}</button>;
  }
}

Cette approche rend le code plus lisible, maintenable et intégré au cycle de vie du composant.

Les Outputs Targets

Une des forces majeures de StencilJS réside dans les Outputs Targets. Ces derniers permettent de générer automatiquement des wrappers pour différents frameworks à partir d’un même Web Component.

Prenons un Web Component classique : pour l’utiliser dans Angular, React ou Vue, il faudrait normalement gérer manuellement la communication entre le composant et le framework : bindings, événements, propriétés… Cela peut vite devenir complexe et répétitif.

Avec StencilJS et ses Output Targets :
- Le composant est automatiquement “traduit” en composant natif du framework.
- Dans Angular, par exemple, nous obtenons un vrai composant Angular avec le binding, la gestion des événements et toutes les conventions Angular.
- Dans React, nous obtenons un vrai composant React avec props et callbacks typiques.

Autrement dit, nous n'utilisons pas juste un Web Component générique, mais un composant intégré au framework, qui respecte ses règles et nous fait gagner énormément de temps.

Exemple Angular avec Output Target :

import { Component, Prop, State, Event, EventEmitter, h } from '@stencil/core';

@Component({
  tag: 'my-counter',
  shadow: true
})
export class MyCounter {
  @Prop() start: number = 0;
  @State() count: number;
  @Event() countChanged: EventEmitter<{ count: number }>;

  componentWillLoad() {
    this.count = this.start;
  }

  private increment = () => {
    this.count++;
    this.countChanged.emit({ count: this.count });
  };

  render() {
    return <button onClick={this.increment}>Compteur : {this.count}</button>;
  }
}

Grâce aux Output Targets, my-counter se comporte comme un composant Angular classique : nous pouvons utiliser le two-way binding, les événements et toutes les fonctionnalités natives d’Angular, sans écrire de code supplémentaire pour adapter le Web Component.

Schéma de la construction des composants multi-framework à partir d’un unique composant Stencil
Schéma de la construction des composants multi-framework à partir d’un unique composant Stencil

Avantages de StencilJS

- Un seul code pour tous les frameworks.
- API simple grâce aux décorateurs et à la syntaxe JSX.
- Intégration facile dans différents frameworks via les Outputs Targets.
- Optimisations intégrées : lazy loading, tree shaking, rendu rapide.

Limites et inconvénients

- Écosystème de plugins plus limité comparé à React ou Vue.
- Moins de sucre syntaxique pour le binding complexe (ex. Angular).
- Courbe d’apprentissage initiale pour configurer un projet Stencil.
- Hot reload parfois limité.

Quand utiliser StencilJS ?

Stencil est particulièrement adapté pour :
- Design systèmes et bibliothèques de composants.
- Applications of multi-frameworks.
- Tout projet nécessitant des composants réutilisables et universels.

StencilJS permet donc de créer des composants standardisés, performants et facilement intégrables partout. Pour les entreprises et équipes cherchant à centraliser et à réutiliser leur UI, StencilJS est une option sérieuse à considérer.