Découvrir Un Nouveau Framework : SolidJS 🧑‍💻

Découvrir Un Nouveau Framework : SolidJS 🧑‍💻

2 septembre 2022 | 14 mins de lecture

Publié sur dev.to et medium.com

Le monde du développement Web est en perpétuelle évolution, et voit apparaître de nouvelles librairies JavaScript (toujours plus intéressantes), jour après jour...

Cette année (une fois encore), je me suis lancé le défi d'expérimenter une nouvelle technologie. Le Web évoluant constamment, il est important de se mettre à la page tous les 6 mois. C'est à l'issue des résultats du dernier sondage "State Of JS", que j'ai repéré deux nouveaux frameworks que me semblaient prometteurs : AlpineJS x SolidJS

NB : Je suis passionné, j'ai soif de connaissances, et récemment j'avais envie d'apprendre quelque chose de frais, à l'image de la découverte de Svelte en 2019.

Après avoir lu deux ou trois documentations, et analysé plusieurs dizaines de lignes de code, mon choix s'est très vite orientée vers SolidJS, notamment du fait de l'omniprésence du JSX, ainsi que de concepts similaires à mon outil de tous les jours, à savoir : React !

Dans cet article, je vais évidemment parler de ce qu'est SolidJS, mais surtout de la manière dont j'ai appréhendé ce nouveau framework dans une démarche d'apprentissage (de la théorie à la pratique). Je ne manquerais pas d'évoquer les dépendances qui gravitent autour de son écosystème. Enfin, nous verrons si ce dernier tient sa promesse de "réactivité fine", tout en étant simple et performant.

SolidJS, c'est quoi !?

NB : Avant d'entrer dans le vif du sujet, une brève explication... Dans le cadre de travaux autour de la mise en place d'un réseau de pair-à-pair en JavaScript (article à venir...), j'ai souhaité disposer d'une interface / d'une application de contrôle des actions relatives à un noeud (autre que Postman). Plutôt que de développer cela à partir de bases déjà connues, j'ai commencé à apprendre plus en détail ce qu'est réellement SolidJS, et à la mettre en oeuvre dans un projet d'ampleur. Voici ma démarche d'apprentissage !

Tout comme une multitude de librairies JavaScript avant lui, SolidJS est un framework orienté composants permettant de construire des interfaces Web. Bien que proche de React dans certains de ses concepts (et sa syntaxe), il se veut plus léger et réactif que ce dernier, à l'instar de Svelte. En effet, SolidJS prône la "réactivité fine" en opérant les changements directement depuis le DOM, plutôt qu'à travers un DOM virtuel.

De ce fait (et d'après les tests de performance de la communauté open-source), cette nouvelle monture serait à la fois plus rapide que React, Vue, Preact (qui possède pourtant d'un DOM virtuel plus léger que React), ainsi que Svelte, et arriverait en tête des scores !

ReactVuePreactSvelteSolidJS
Create Rows1.33ms1.19ms1.20ms1.26ms1.05ms
Create Many Rows1.74ms1.27ms1.31ms1.26ms1.14ms
Partial Update1.37ms1.20ms1.20ms1.11ms1.04ms

Création de 1 000 / 10 000 éléments, et modification de 100 éléments (millisecondes)

ReactVuePreactSvelteSolidJS
Ready Memory1.20MB1.14MB1.04MB1.01MB1.02MB
Run Memory3.22MB2.42MB2.76MB1.71MB1.57MB
Run Memory (Update)3.62MB2.41MB2.76MB1.67MB1.58MB

Allocation mémoire au chargement, à la création et à la modification (mégabytes)

Débuter avec SolidJS

Débuter avec SolidJS est un jeu d'enfant (je recommande malgré tout d'avoir les bases de JavaScript, ainsi que la maitrise de certaines notions de framework orienté composants, pour bien comprendre ce qu'est la réactivité...). Il suffit de faire appel à la librairie de Rich Harris (degit), afin d'échafauder le projet. Ainsi, l'environnement JavaScript sera pré-configurée pour fonctionner à partir d'un bundler, non pas Webpack cette fois-ci, mais bien Vite !

Propulsé majoritairement par la communauté de Vue, Vite est un outil JavaScript d'un nouveau genre, vous permettant de démarrer un nouveau projet Web extrêmement rapidement, tout en proposant un environnement moderne. Inspiré de RollupJS (mais aussi ESBuild), Vite se dote de nombreuses fonctionnalités, à savoir :

  • La gestion des dépendances NPM
  • La pre-compilation des sources (lui permettant de gagner énormément de temps au démarrage)
  • Le remplacement des modules à chaud (HMR)
  • Le support natif de TypeScript
  • La transpilation de JSX à la volée
  • Le support des modules CSS
  • La gestion dynamique des imports
  • L'optimisation de la compilation, etc...

NB : L'écosystème de Vite apporte également Vitest pour les tests unitaires, en alternative à Jest.

SolidJS offre une large palette de templates permettant d'initialiser un nouveau projet, avec le choix du langage : soit JavaScript, soit TypeScript ; l'intégration d'une librairie UI prête à l'emploi (notamment l'excellentissime TailwindCSS), mais aussi l'implémentation d'un moteur de tests unitaires : Jest / Vitest (au choix).

import { render } from 'solid-js/web';
import { createSignal, onMount, onCleanup } from 'solid-js';

const CountingComponent = () => {
  const [count, setCount] = createSignal(0);
  const interval = setInterval(() => {
    setCount(count => count + 1);
  }, 1000);

  onMount(() => console.log('Mounted'));
  onCleanup(() => clearInterval(interval));

  return <div>Count value is {count()}</div>;
};

render(() => <CountingComponent />, document.getElementById('root'));

Composant de compteur avec SolidJS

import { render } from 'react-dom';
import { useState, useEffect } from 'react';

const CountingComponent = () => {
  const [count, setCount] = useState(0);
  const interval = setInterval(() => {
    setCount(count => count + 1);
  }, 1000);

  useEffect(() => {
    console.log('Mounted');
    return (cleanup = () => clearInterval(interval));
  }, []);

  return <div>Count value is {count}</div>;
};

render(() => <CountingComponent />, document.getElementById('root'));

Composant de compteur avec React

Ci-dessus, voici à quoi peut ressembler un premier composant de compteur simple avec SolidJS, et son opposé côté React. On s'aperçoit très vite des similitudes qui existent entre ces deux librairies. Et pourtant, SolidJS se veut plus explicite et simple !

En effet, plutôt que d'avoir un hook useEffect qui permet à la fois de gérer le montage et le démontage du composant (au travers une fonction de cleanup), SolidJS expose ses actions avec les fonctions de son cycle de vie : onMount et onCleanup.

De même, l'API createSignal est relativement identique au hook useState de React dans son fonctionnement, puisque le flux de données est unidirectionnel. D'un côté l'accesseur (immutable), et de l'autre le "setter". Pourtant, il existe une différence non négligeable avec SolidJS ! La notion de "réactivité fine" pars du principe que les composants ne se rendent qu'une fois, et que les fonctions associées aux Signals sont exécutées dès lors que ces derniers sont mis à jour. Les changements seront ensuite impactés directement dans le DOM. Puisque l'accès à la valeur d'un Signal ne se fait qu'en l'appelant (présence des parenthèses), je considère davantage l'accesseur comme un "getter", plutôt qu'une constante... Les Signals sont le concept clé de SolidJS !

Dans l'ensemble SolidJS est vraiment simple à prendre en main, l'apprentissage de son API est également facilité du fait d'une documentation enrichie et d'une partie "playground" permettant de comprendre chaque aspect de cette librairie, et de monter en compétence petit à petit.

Bien qu'élémentaire, SolidJS est un framework complet, puisqu'il propose aussi une solution de routage (npm install solid-app-router), une fonctionnalité de "code splitting" grâce au chargement asynchrone des composants (rendu possible grâce au bundler), ou encore la possibilité de rendre son application côté serveur (SSR). Ainsi SolidJS, se classe parmi les grands (React, Vue, etc...) en tant que librairie Web pour créer des interfaces utilisateurs.

Construire des interfaces utilisateur

Anatomy Of Modal

Assez de théorie, passons maintenant à la pratique ! Et quoi de mieux que la mise en pratique que d'évoluer dans un cas concret... Pour cela, je vous propose de créer un nouveau projet, pour gérer l'alimentation et l'affichage d'une modale, afin d'aborder les notions de réactivité de SolidJS. C'est parti !

npx degit solidjs/templates/js-vitest my-awesome-app
cd my-awesome-app
npm install

Et voilà ! Vous devriez voir apparaitre un nouveau répertoire (my-awesome-app dans mon cas), léger en termes de dépendances, mais déjà pré-configuré pour fonctionner /vit/ !

NB : Dans le cadre de mes travaux, j'ai aussi ajouté le support de TailwindCSS à l'initialisation du projet (npx degit solidjs/templates/js-tailwindcss), puis fusionné les deux configurations de Vite manuellement.

import { mergeProps, Switch, Match, Show } from 'solid-js';
import { IconInfo, IconSuccess, IconWarning, IconError } from './icons';

export const Modal = (props) => {
  const merged = mergeProps({
    type: 'INFO',
    title: 'Info',
    onOk: () => console.log('Ok')
  }, props);

  return (
    <div class="container">
      <div class="modal__background" />

      <div class="modal">
        <span class="modal__icon">
          <Switch fallback={<IconInfo class="text-blue-700 bg-blue-100" />}>
            <Match when={merged.type === 'SUCCESS'}>
              <IconSuccess class="text-green-700 bg-green-100" />
            </Match>

            <Match when={merged.type === 'WARNING'}>
              <IconWarning class="text-orange-700 bg-orange-100" />
            </Match>

            <Match when={merged.type === 'ERROR'}>
              <IconError class="text-red-700 bg-red-100" />
            </Match>
          </Switch>
        <span>

        <span class="modal__title">{merged.title}</span>

        {props.children}

        <div class="modal__actions">
          <Show when={typeof props.onCancel === 'function'}>
            <button class="modal__action-cancel" type="button" onClick={props.onCancel}>
              Cancel
            </button>
          </Show>

          <button class="modal__action-ok" type="button" onClick={merged.onOk}>
            Ok
          </button>
        </div>
      </div>
    </div>
  );
}

Modal.jsx

La modale représente un composant stateless qui s'étend visuellement sur tout l'écran (si on considère l'arrière-plan assombri et le centrage de l'élément principal), et qui n'attend que des valeurs via les props pour prendre vie. Ceci facilite aussi les tests unitaires avec Vitest (et Testing Library).

L'API de SolidJS peut rendre la syntaxe un peu verbeuse à première "Vue" 😉, mais ses composants sont indispensables afin d'opérer un changement dans le DOM, en l'absence de DOM virtuel. Ainsi sa simplicité repose sur une combinaison astucieuse de Signals (immutables) et de composants (<Show>, <Switch>, <For>, etc...) pour rendre le DOM réactif aux endroits souhaités !

NB : À noter qu'ici, on utilise l'API mergeProps afin d'imposer des valeurs par défaut aux props. Là encore, il est important d'utiliser cette fonction pour conserver la réactivité, plutôt que d'initialiser ces mêmes valeurs directement depuis la signature du composant (comme avec React).

import { render } from 'solid-js/web';
import { createSignal, onMount, Show } from 'solid-js';
import { Modal } from './Modal';

export const App = () => {
  const [showModal, setShowModal] = createSignal(false);
  const [modalType, setModalType] = createSignal('');
  const [modalTitle, setModalTitle] = createSignal('Lorem ipsum');

  onMount(() => {
    if (showModal()) {
      setTimeout(() => {
        setModalType('SUCCESS');
        setModalTitle('Everything is good');
      }, 1500);

      setTimeout(() => {
        setModalType('ERROR');
        setModalTitle('Something is wrong');
      }, 3000);
    }
  });

  return (
    <div class="app">
      <Show when={showModal()}>
        <Modal
          type={modalType()}
          title={modalTitle()}
          onOk={() => {
            setModalType('WARNING');
            setShowModal(false);
          }}>
          <p>Lorem ipsum dolor sit amet</p>
        </Modal>
      </Show>

      <button
        style={{ margin: 'auto' }}
        onClick={() => setShowModal(currentVal => !currentVal)}>
        {showModal() ? 'Hide Modal' : 'Show Modal'}
      </button>
    </div>
  );
};

render(() => <App />, document.getElementById('app'));

main.jsx

Pour ce qui est de l'usage, le déclenchement de l'affichage de la modale (ainsi que son alimentation) s'opère via des Signals, depuis le composant parent. Ci-dessus, je me suis amusé à changer l'aspect de la modale en fonction du temps, pour observer la réactivité. À nouveau, cela peut sembler verbeux, et pourtant c'est simple et efficace !

Ce mode de fonctionnement, n'est parfois pas suffisant, notamment lors du développement d'une application à l'échelle, ou bien lorsqu'on utilise un système de routage (npm install solid-app-router), pour séparer les pages de son application. La gestion de la modale, serait possible en utilisant une logique de "props drilling" (parent vers enfant, enfant vers sous-enfant, etc...), mais on tomberait vite dans une complexité liée à l'imbrication de composants, ainsi qu'à leur maintenance... Pas de problème ! Pour ce qui est des modales, il y a le composant <Portal> ; pour tout le reste, il y a l'API "Context" !

import { mergeProps, Switch, Match, Show, Portal } from 'solid-js';
import { IconInfo, IconSuccess, IconWarning, IconError } from './icons';

export const Modal = props => (
  <Portal mount={document.getElementById('modal')}>
    <ModalComponent {...props} />
  </Portal>
);

// Move <Modal /> To <ModalComponent />
export const ModalComponent = props => {
  const merged = mergeProps(
    {
      type: 'INFO',
      title: 'Info',
      onOk: () => console.log('Ok')
    },
    props
  );

  return <div class="container">{/* ... */}</div>;
};

Modal.jsx

L'API Context

NB : Dans un précédent article, j'explique comment il est possible de mettre en oeuvre un Redux "maison", avec l'API Context de React, ici la logique est la même...

import { createContext, createEffect, createSignal, useContext } from 'solid-js';
import { createStore } from 'solid-js/store';

const store = createContext();

const { Provider } = store;

export const useStore = () => {
  return useContext(store);
};

const StateProvider = props => {
  const [currentIdx, setCurrentIdx] = createSignal(0);
  const [state, setState] = createStore({
    email: 'morty.smith@pm.me'
  });

  createEffect(() => {
    console.log('currentIdx', currentIdx());
  });

  const value = [
    {
      currentIdx,
      fieldValues: state
    },
    {
      incrementIndex() {
        setCurrentIdx(prevCurrentIdx => prevCurrentIdx + 1);
      },
      decrementIndex() {
        setCurrentIdx(prevCurrentIdx => prevCurrentIdx - 1);
      },
      getField(key) {
        return state[key] || '';
      },
      setField(key, value) {
        setState({ [key]: value });
      }
    }
  ];

  return <Provider value={value}>{props.children}</Provider>;
};

export default StateProvider;

StateProvider.jsx

import { Form, InputField } from './components';
import { useStore } from './StateProvider';

const StateConsumer = () => {
  const [{ currentIdx }, { getField, setField }] = useStore();

  return (
    <Form index={currentIdx()}>
      <InputField
        label="Email"
        type="email"
        placeholder="rick.sanchez@pm.me"
        defaultValue={getField('email')}
        handleChange={e => setField('email', e.target.value)}
      />
    </Form>
  );
};

StateConsumer.jsx

Le State Management (via l'API Context), est un paradigme de programmation réactive idéal pour les développements volumineux, permettant d'éviter le problème de "props drilling", mais aussi de présence de données en double / triple... De cette manière, on dit que le "store" est unique source de vérité de l'application.

NB : Pour en savoir plus sur ce sujet, n'hésitez pas à consulter le code source de mon projet sur Gitlab (et Vercel).

Vous reprendrez bien un supplément de tests unitaires ?

L'écosystème propulsé par SolidJS, prône des technologies modernes telles que PNPM, Vite, Vitest mais aussi Testing Library. En s'appuyant sur l'implémentation de Preact, l'équipe de SolidJS apporte le framework de tests unitaires orientés composants, qui a déjà fait ses preuves avec React, Vue, Angular, Cypress, etc...

import { render, fireEvent } from 'solid-testing-library';
import { describe, it, expect, vi } from 'vitest';
import { Modal } from './Modal';

describe('<Modal />', () => {
  it('Should Render', () => {
    const { getByText, unmount } = render(() => (
      <Modal title="> Modal Title Goes Here <">
        <p>Lorem ipsum dolor sit amet</p>
      </Modal>
    ));

    expect(getByText('> Modal Title Goes Here <')).toBeInTheDocument();
    expect(getByText('Lorem ipsum dolor sit amet')).toBeInTheDocument();
    unmount();
  });

  it('Should Trigger onClick Event(s)', async () => {
    const mockedOnCancel = vi.fn();
    const mockedOnOk = vi.fn();

    const { getByText, unmount } = render(() => (
      <Modal
        type="WARNING"
        title="Warning"
        onCancel={mockedOnCancel}
        onOk={mockedOnOk}
      />
    ));

    const cancelBtn = getByText('Cancel');
    fireEvent.click(cancelBtn);
    expect(mockedOnCancel).toHaveBeenCalled();

    const okBtn = getByText('Ok');
    fireEvent.click(okBtn);
    expect(mockedOnOk).toHaveBeenCalled();

    unmount();
  });
});

Modal.test.jsx

Simplicité + Réactivité === Performance

J'ai vraiment apprécié jouer avec ce nouveau framework (comme souvent avec les technologies JavaScript). On y retrouve beaucoup de notions de ses homologues, ainsi que des nouveaux telles que les Signals, ou encore la fonction createResource (très pratique pour le requêtage d'API). Je n'ai pas eu de grandes difficultés à créer mon application de contrôle de noeud (de A à Z), puisque ce dernier offre une grande flexibilité dans l'organisation du projet. L'expérience développeur est donc optimale !

NB : À l'inverse de React (ou Vue), SolidJS compile son code dès la phase de développement (comme Svelte). De cette manière, il est en capacité d'opérer efficacement et rapidement les changements dans le "vrai" DOM ; un point supplémentaire en faveur de l'expérience développeur ! 👍

Ce premier tour d'horizon me donne envie d'en savoir plus et de proposer la démarche dans un cas d'entreprise. Peut-être en remplacement de Preact, ou même de React... SolidJS ne propose pas que les composants et fonctionnalités démontrés ci-dessus. Si je devais faire une liste non exhaustive, cette librairie propose également :

  • Le rendu dynamique d'une liste d'éléments, depuis (non pas Array.prototype.map mais) son composant <For>
  • L'encapsulation des appels asynchrones (en tant que ressources), pour une meilleure visibilité de l'état de chargement, ainsi que la présence (ou non) d'erreur(s)
  • L'affichage d'interfaces en cas de chargement lâche (composants ou ressources), via le composant <Suspense>
  • L'affichage d'interfaces en cas d'erreur(s), afin d'éviter le crash de l'application
  • Le mode de fonctionnement SSR, pour des gains de performance, mais aussi pour un meilleur SEO (ici SolidJS, marche sur les plate-bandes de Nuxt, Next ou encore SvelteKit)
  • Ou encore, la réconciliation de "store", permettant de faire cohabiter Redux dans une application à l'échelle

Du fait de ses concepts simples à appréhender, et malgré l'absence de DOM virtuel, SolidJS a su prouver sa force et sa stabilité (en 5 ans de développement), en poussant la réactivité en sa forme la plus pure : les Signals ! Tout comme Svelte, ce framework tourné vers l'avenir a de beaux jours devant lui. Coup de ❤️ de l'année !