Blog

“Schrijf code eenmalig en gebruik het overal” met Mitosis; een mooie droom of werkelijkheid?

Mitosis is een tool die JSX-componenten omzet in volledig functionele componenten voor frameworks zoals React, Vue, Angular en nog veel meer. Het is een ambitieus project dat heel gunstig kan zijn voor veel ontwikkelaars. Maar hoe bereiken ze hun doel, en hoe geschikt is het voor productie?

Het door Builder.io ontwikkelde Mitosis is een veelbelovend open-source tool. Het streeft ernaar een probleem op te lossen waar elke design system developer van droomt: schrijf code één keer, en gebruik het overal. Mitosis probeert dit doel te bereiken door JSX lite (een statische subset van JSX) om te zetten naar een Abstract Syntax Tree (AST). Deze AST fungeert als een neutraal medium dat niet gerelateerd is aan een specifiek framework. Mitosis biedt compilers voor elk ondersteund output-target. Zie deze compilers als scripts die JSX lite code als input accepteren en code genereren voor een specifiek framework target.

Op het moment dat ik dit schrijf ondersteunt Mitosis de volgende output-doelen:

Diagram met alle mogelijke Mitosis compilatie targets
Lijst van alle mogelijke Mitosis compilatie targets

Deze indrukwekkende lijst is precies de reden waarom ik Mitosis ben gaan onderzoeken om te zien of we het kunnen gebruiken om onze design systems te verbeteren.

De voordelen

Bij De Voorhoede hebben we veel ervaring met het bouwen van design systems. Een van de grootste uitdagingen in onze design system projecten is het ondersteunen van meerdere frameworks¹.

Tijd besparen 

Het afgelopen jaar heb ik gewerkt aan een design system dat React, Vue en Angular moest ondersteunen. Deze frameworks hebben elk hun eigen syntaxis en benaderingen voor dezelfde problemen. Het was makkelijk om styling te scheiden en te hergebruiken, maar markup en gedrag zijn vaak zo nauw geïntegreerd in de frameworks dat veranderingen doorgaans vereisen dat je de code in drie componenten test en wijzigt (één voor elk framework). Dit zorgt ervoor dat zelfs de kleinste wijzigingen snel veel tijd kosten.

Mitosis is bedoeld om ontwikkelaars de moeite te besparen om dezelfde componenten voor verschillende frameworks te maken en te onderhouden. Wat, sprekende uit ervaring, de ontwikkeling van een design system aanzienlijk kan versnellen.

Onderstaand een voorbeeld van een Mitosis-component dat wordt omgezet naar verschillende frameworks:

// HelloWorld.lite.tsx :: een Mitosis component geschreven in JSX lite
import type { HelloWorldProps } from './model';

export default function HelloWorld(props: HelloWorldProps) {
  return (
    <h1>{props.greeting} World</h1>
  );
}

Dat ziet er gecompileerd zo uit:

// HelloWorld.tsx :: de React output
import * as React from "react";
import type { HelloWorldProps } from "./model";

function HelloWorld(props: HelloWorldProps) {
  return <h1>{props.greeting} World</h1>;
}

export default HelloWorld;
// HelloWorld.vue :: de Vue output
<template>
  <h1>{{ greeting }} World</h1>
</template>

<script setup lang="ts">
import type { HelloWorldProps } from "./model";

const props = defineProps<HelloWorldProps>();
</script>
// HelloWorld.js :: de Angular output
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";

import { Component, Input } from "@angular/core";

@Component({
  selector: "hello-world, HelloWorld",
  template: `
    <h1>{{greeting}} World</h1>
  `,
  styles: [
    `
      :host {
        display: contents;
      }
    `,
  ],
})
export default class HelloWorld {
  @Input() greeting;
}

@NgModule({
  declarations: [HelloWorld],
  imports: [CommonModule],
  exports: [HelloWorld],
})
export class HelloWorldModule {}

Minder specifieke kennis nodig

Als ontwikkelaars alleen Mitosis-componenten hoeven te maken (en dus JSX lite schrijven), zou er minder behoefte zijn aan gedetailleerde kennis van de frameworks die ze in hun design system ondersteunen (hoewel ze wel kennis moeten hebben van Mitosis zelf). Natuurlijk zijn er altijd situaties waarin je de omgezette code moet debuggen, wat kennis vereist van het framework, maar dit zou eerder de uitzondering dan de regel zijn.

Dit biedt aanzienlijke voordelen voor zowel ontwikkelaars als bedrijven, aangezien het onderhoud van een design system minder arbeidsintensief wordt.

De juiste mindset

Toen ik mijn eerste Mitosis project begon, dacht ik dat het lezen van de documentatie voldoende zou zijn om te kunnen beginnen. Het is tenslotte praktisch hetzelfde als het schrijven van React code. Toch?

Fout! JSX lite is iets dat, hoewel een subset van JSX, vereiste behoorlijk wat moeite voordat ik resultaten begon te zien. Ik dacht dat het schrijven van JSX en het vervangen van React-specifieke concepten door Mitosis-concepten het grootste deel zou dekken. Dus ik schreef een component in JSX, verving alle hooks die je normaal uit React importeert (useState, useEffect, useRef, enz.) door hun Mitosis-tegenhangers (useState/useStore, onUpdate, useRef respectievelijk), maar dit was niet voldoende.

Mitosis is namelijk erg strikt op gebied van code interpretatie. Elk stukje code dat geen deel uitmaakt van je template of een van de Mitosis hooks/lifecycle events, komt niet in de gegenereerde code terecht. Dit betekent dat je, om je gegenereerde componenten te laten werken, de strikte regels van Mitosis moet volgen. Dit kan erg moeilijk zijn, omdat veel van deze problemen onzichtbaar zijn op het moment dat je de code schrijft. Dit komt omdat de IDE je code interpreteert als JSX². Je code is geldig volgens de taal server, maar wanneer je de Mitosis compiler vraagt om z’n magie te doen, begint het te schreeuwen met foutmeldingen.

Ik vond dit ontmoedigend toen ik begon. Het laatste wat je wilt, is dat je eerste component na het compilen al fouten vertoont voordat je testapplicatie überhaupt is opgestart. Maar als je erover nadenkt, is het niet zoveel gevraagd. Het is tenslotte een eigen tool dat dezelfde onderdelen als een framework uit de handen probeert te nemen. Met dit in gedachten probeerde ik open te staan voor de concepten en valkuilen van Mitosis.

Leer dus van mijn fouten. Probeer niet te denken aan Mitosis als een tool die JSX (lite) gebruikt, maar zie het als een framework waarin je moet investeren om het echt te begrijpen en ervan te profiteren. Mitosis biedt een eslint plugin om dit iets eenvoudiger te maken.

De nadelen

Maar zelfs nadat ik in de juiste mindset zat, liep ik nog steeds problemen tegen aan.

Gebrek aan controle 

Als ontwikkelaar ben je gewend om controle te hebben over de meeste code die je levert. Bij Mitosis is dit een beetje anders. Je schrijft de input (Mitosis-componenten), maar de compiler genereert de code die je uiteindelijk gebruikt op productie. Dit kan lastig zijn, aangezien de Mitosis-compilers niet compleet en/of onduidelijk gedocumenteerd zijn (let op dat de documentatie geen volledige lijst van valkuilen/eigenaardigheden bevat). In mijn ervaring — zal er naast werkende code, ook code zijn die ik anders zou schrijven, moeilijk te begrijpen is of simpelweg niet werkt³.

Het debuggen hiervan was moeilijk. Mitosis is open-source, dus je kunt de broncode van de relevante compiler opzoeken, maar het debuggen van compilers is niet iets waar ik aan gewend was.

Compatibiliteit 

Naast het gebrek aan controle over hoe de gegenereerde code eruit ziet, heb je ook weinig controle over versiebeheer. Dit kan leiden tot aanzienlijke problemen, aangezien je niet van alle afnemers van je design system kunt verwachten dat ze altijd de meest recente versie van hun framework gebruiken. De gebruikers van het design system gebruiken vaak ook een meta-framework en/of code van derden die ook compatibel moeten zijn met dezelfde framework-versie.

Mitosis biedt enige controle over de output, bijvoorbeeld:

// Voorbeeld van een Mitosis config 
module.exports = {
  files: "src/**",
  targets: ['vue', 'react'],
  dest: "../output",
  options: {
    react: {
      typescript: true,
      stylesType: 'style-tag',
    },
    vue: {
      typescript: true,
      api: 'composition',
    }
  }
}

Maar er is momenteel geen manier om te specificeren met welke framework-versie je compatibel wilt zijn. Iets dat je meestal wilt vastzetten en alleen wilt wijzigen wanneer het team besluit dit te doen.

Ontwikkelaarservaring

De bovengenoemde pijnpunten laten veel te wensen over op het gebied van ontwikkelaarservaring. Dit kun je accepteren, maar dat is meer een afweging tussen bedrijfsbelang en ontwikkelaarservaring.

Debuggen

Het testen en debuggen van je Mitosis-code is lastig. Mitosis is JSX (lite) en wordt niet in de browser uitgevoerd. De aanbevolen methode voor debuggen is het omzetten van je Mitosis-code naar bijvoorbeeld React, en vervolgens de gegenereerde code in een React-app te importeren om deze te testen. Dit is echter een omslachtig proces dat een verworpen beeld van je code oplevert, omdat:

  • De compilatie-stappen proberen niet de gegenereerde code te valideren — wat betekent dat Mitosis React-componenten genereert, maar niet controleert of deze bestanden valide zijn of syntax fouten bevatten.
  • Je weet niet of de fout veroorzaakt wordt door code in Mitosis of in React.
  • console.log en dergelijke code wordt door de compiler verwijderd (momenteel zijn er geen configuratie-opties om deze te behouden).

Voorbeeld van een Mitosis-component met een console.log() regel die door de compilers wordt verwijderd:

// MyComponent.lite.tsx :: een mitosis component met een console.log() statement
import { useStore } from '@builder.io/mitosis';
import type { MyComponentProps, MyComponentState } from './model';

export default function MyComponent(props: MyComponentProps) {
  const state = useStore<MyComponentState>({
    greeting: props.greeting ?? 'Hello',
  });

  console.log(`I am using the ${props.greeting ? 'value from the prop' : 'default value'} as a greeting`);

  return (
    <h1>{state.greeting} World</h1>
  );
}

dat ziet er in React zo uit:

// MyComponent.tsx :: de React output waar de console.log() statement mist 
"use client";
import * as React from "react";
import { useState } from "react";
import type { MyComponentProps, MyComponentState } from "./model";

function MyComponent(props: MyComponentProps) {
  const [greeting, setGreeting] = useState(() => props.greeting ?? "Hello");

  return <h1>{greeting} World</h1>;
}

export default MyComponent;

Mijn oordeel voor nu

En nu, om je brandende vraag te beantwoorden, voldoet Mitosis aan zijn ambities?

Persoonlijk zou ik nog geen design system creëren met Mitosis als basis. Door het gebrek aan controle over de output wil ik dat de compilers vollediger zijn voordat ik Mitosis een cruciaal onderdeel van mijn design system maak.

Laat dit je echter niet afschrikken. Mitosis kan zeker worden gebruikt als een hulpmiddel bij de ontwikkeling van design systems. Als je bijvoorbeeld componenten in React, Vue en Angular moet maken, kan het waardevol zijn om een Mitosis-component te creëren, deze om te zetten naar de verschillende frameworks en deze output als startpunt voor je componenten te gebruiken. Op deze manier kun je veel boilerplate-code vermijden en je tijd besteden aan het aanpassen en verfijnen van de output naar wens. Dit kan veel tijd besparen voor je developers, afhankelijk van het aantal frameworks dat je design system moet ondersteunen.

Ik wil zeker niet mensen die aan het Mitosis-project hebben bijgedragen schenden. Zoals eerder vermeld, is het project zeer ambitieus en heeft het veel potentie. Ik ben blij om te zien dat het als open-source tool wordt ontwikkeld, omdat ik denk dat het een uniek idee is waar veel mensen van kunnen profiteren. De enorme reikwijdte maakt het schrijven (en onderhouden) van de compilers erg resource-intensief; het feit dat Mitosis momenteel zo goed werkt, is al een prestatie op zich. Ik hoop dat het in de komende jaren verder zal groeien en dat deze blogpost de lezers niet afschrikt om Mitosis zelf uit te proberen. Als je geïnteresseerd bent om bij te dragen, weet ik zeker dat dit in de community zeer gewaardeerd wordt en dat bijdragen aan het project erg op prijs wordt gesteld. Daarnaast is het een goede leerervaring voor developers.

Hoewel ik Mitosis nog niet als kernonderdeel van een design system zou opnemen, zijn er al bedrijven die Mitosis praktijk gebruiken. Een voorbeeld dat ik tegenkwam is DB UI Mono (het design system van Deutsche Bahn). Het lijkt daar goed te functioneren, dus neem mijn conclusie met een korreltje zout.

TL;DR

Mitosis is een open-source tool dat gericht is op het omzetten van JSX-componenten naar functionele componenten voor meerdere frameworks zoals React, Vue en Angular. Het biedt voordelen zoals tijdbesparing en minder noodzaak voor framework-specifieke kennis. Het brengt echter ook uitdagingen met zich mee, waaronder een steile leercurve, strikte regels, debugproblemen en gebrek aan controle over de output. Hoewel veelbelovend, is Mitosis nog geen perfecte oplossing voor ontwikkeling over meerdere frameworks en vereist het zorgvuldige overweging voordat het in productieomgevingen wordt geïmplementeerd.

¹ Web Componenten hebben geprobeerd deze leegte op te vullen door een standaard component model voor het web te bieden, wat het afschermen van componenten en het samenwerken tussen verschillende systemen mogelijk maakt, maar ze hebben ook hun eigen beperkingen.

² Mitosis gebruikt een .lite.jsx naamgevingsconventie. Omdat Mitosis echter nog niet zo populair is, valideert mijn IDE de code als React (lees JSX) code. Je kan TS configureren om Mitosis’ JSX types te gebruiken, wat sommige van deze problemen oplost.

³ Dit gedrag kan je aanpassen door de krachtige Mitosis plugins te gebruiken, maar dit kost wel een stuk meer tijd en moeite aangezien je eigenlijk de Mitosis compilers aan het uitbreiden bent met je eigen logica.

⁴ Sami Jaber van het Mitosis team gaf terecht aan dat je de onInit hook kan gebruiken om hetzelfde gedrag te bereiken in Mitosis. Maar dat neemt niet weg dat, de vervelende Mitosis ‘gotcha’ waar alle code die in de render body van een component staat niet meegenomen wordt in de output, nog steeds bestaat (terwijl dit een valide use case is om constants/magic-numbers te definiëren).

Gerelateerde blog posts

← Alle blogposts