Blog

“Write components once, run everywhere” with Mitosis; a beautiful dream or reality?

Mitosis is a tool that transforms JSX components into fully functional components for frameworks such as React, Vue, Angular and many more. It’s an ambitious project that could benefit many developers. But how does it achieve its goal and how production ready is it?

Developed by Builder.io, Mitosis is a promising open-source tool. It aims to solve a problem that every design system developer has dreamt of; write code once, run everywhere. It attempts to reach this goal by transforming JSX lite (a static subset of JSX) to an Abstract Syntax Tree (AST). This AST serves as a neutral format which is not related to any specific framework. Mitosis provides compilers for each output target that they support. Think of these compilers as transformative scripts that accept Mitosis ASTs as input and generate code for a specific framework target. 

At the time of writing Mitosis supports the following output targets:

Diagram showing the compile targets of Mitosis
List of all the possible Mitosis compilation targets

This impressive list is exactly the reason why I decided to explore Mitosis and figure out whether we can use it to improve our design system setups.

The Good

Here at De Voorhoede we have some experience building design systems. One of the biggest challenges in our design system projects is supporting multiple frameworks¹.

Saving time

Over the last year I've worked on a design system that had to support React, Vue and Angular. These frameworks all have their own syntax and approaches for the same problems. Styling was easy to separate and reuse, but markup and behavior are often so tightly integrated in the framework syntax that changes typically require testing and changing the code in three components (one for each framework). This causes even the smallest changes to quickly become time intensive.

Mitosis strives to eliminate the need for developers to create (and maintain) the same components across different frameworks, which, speaking from experience, would significantly speed up the development of a design system.

Here’s an example of a Mitosis component being compilers to different frameworks:

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

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

Compiles to:

// HelloWorld.tsx :: The 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 :: The Vue ouput
<template>
  <h1>{{ greeting }} World</h1>
</template>

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

const props = defineProps<HelloWorldProps>();
</script>
// HelloWorld.js :: The Angular ouput
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 {}

Less framework knowledge required

If developers only need to create Mitosis components (and thus write JSX lite) — there would be less need for developers to know the ins and outs of the frameworks they support in their design system (although they would need to have knowledge of Mitosis itself). Of course there will always be cases where you need to debug the compiled code, requiring knowledge of the framework's workings, but this would be the exception rather than the rule.

All in all, these are substantial benefits for both developers and companies, making the task of having a design system a bit less resource intensive.

Getting into the right mindset

To start off my first Mitosis project, I figured all I really needed was to read the Mitosis documentation. After all, it is pretty much the same as writing React code. Right?

Wrong! JSX lite is something that, although a subset of JSX, required quite some getting used to before I started getting results. I thought that writing JSX and swapping React specific concepts for Mitosis concepts would cover the largest part. So I would write a component in JSX, replace all hooks that you normally import from React (useState, useEffect, useRef etc.) with their Mitosis counterparts (useState/useStore, onUpdate, useRef respectively), but this was not sufficient. 

As it turns out, the interpretation of Mitosis is very strict. Every bit of code that is not either part of your template or one of the Mitosis hooks/lifecycle events will not end up in your end code. This means that for your compiled components to work, you have to follow the rigid rules of Mitosis. This can be very difficult because many of these problems are invisible at the moment of writing the code. This has to do with the IDE interpreting your code as JSX². So your code is valid according to the language server, but when you ask the Mitosis compiler to work its magic, it starts screaming like hell.

I found this off putting when I started out. The last thing you want to happen after compiling your first component is running into errors before your test app has even booted up. But when you think about it. It isn’t all that much to ask. After all, it very much is its own tool and takes on the task that your framework normally would. With this in mind I tried to be more open minded to its concepts and gotchas

So learn from my mistakes. Try to not think about it as a tool that uses JSX (lite), rather think of it as a framework which you need to invest some time into to really understand and benefit from. Mitosis provides an eslint plugin to make this a bit easier.

The Bad

But even after getting in the right mindset, I still ran into problems.

Lack of control

As a developer you are used to having fine grained control over most code you ship. With Mitosis this is a bit different. You write the input (Mitosis components), but the compiler generates the code that you will ship. This can be a tough pill to swallow as the Mitosis compilers are not complete and/or clearly documented (note that the documentation does not have a complete list of gotchas/quirks). In my experience — besides the working code, there will be code that I would write differently, is hard to grasp or flat out does not work³. 

Debugging this was difficult. Mitosis is open source, so you can pull up the source-code for the relevant compiler, but debugging compilers is not something I was used to, when writing basic UI components. 

Compatibility

Besides not having much control over what the generated code looks like, you also do not have much control over versioning. This can lead to some pretty substantial problems since you cannot expect all consumers of your design system to always be on the most recent version of their framework.The design system consumers often also use a meta-framework and/or 3rd party code which also has to be compatible with the same framework version. 

Mitosis offers some control over the output, for example:

// Example of a mitosis config 
module.exports = {
  files: "src/**",
  targets: ['vue', 'react'],
  dest: "../output",
  options: {
    react: {
      typescript: true,
      stylesType: 'style-tag',
    },
    vue: {
      typescript: true,
      api: 'composition',
    }
  }
}

But there is currently no way to specify which framework version you want to be compatible with. Something that you generally would want to be locked down and only change when the team decides to do so.

Developer Experience

The above mentioned pain points leave a lot to wish for in the department of developer experience. That might be worth the trade off, but that’s more of a business side vs developer experience discussion.

Debugging

Testing and debugging your Mitosis code can be a pain in the ass. Mitosis is JSX (lite) and does not run in the browser. The recommended method of debugging is compiling your Mitosis code to let’s say React, then importing the compiled code in a React app to test it. But this is a tedious process as:

    • compile steps do not attempt to validate the generated code — meaning that Mitosis will generate React components, but does not check if those files are valid or have syntax errors
    • you do not know if the error is caused by code in Mitosis or React.
    • console.log statements are removed in the compiler (currently no configuration options are available to persist these)⁴.

    Example of a Mitosis component with a console.log() statement that is stripped by the compilers:

    // MyComponent.lite.tsx :: a Mitosis component with a 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>
      );
    }

    Compiles to:

    // MyComponent.tsx :: the React output where the console.log() statement is absent 
    "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;

    My verdict for now

    And now to answer the question you’ve been asking — does Mitosis deliver on its aspirations?

    Personally I would not create a design system with Mitosis as its core yet. With the lack of control over the output, I’d want the compilers to be more feature complete before basing my design system around it. 

    But do not let this deter you. Mitosis can very well be used as a tool for developing design systems. For example, if you need to create a component in React, Vue and Angular; I think creating a Mitosis component, compiling it to the respective frameworks and using this output as a starting point for your components also has a lot of value. This way you can get a lot of the boilerplate code out of the way and spend your time patching up and refining the output to your desire. I believe this could potentially still save your developers a lot of time depending on how many frameworks your design system has to support.

    In no way do I mean any disrespect to anyone who has worked on the Mitosis project. As I have mentioned before — the project is very ambitious and has a lot of potential. I am very pleased to see it being developed as an open source tool as I think it is a very unique idea that many people can benefit from. The huge scope makes the task of writing (and maintaining) the compilers very resource intensive; the fact that Mitosis works as well as it currently does is an accomplishment. I hope to see it grow further in the coming years and hope that the readers of this blog post are not deterred from trying Mitosis themselves. If you are interested in contributing, I know for a fact they value the community a lot and contributing to the project will be appreciated greatly besides being a learning experience.

    Besides me not being ready to adapt Mitosis as an integral part of a design system yet, there are companies already using Mitosis in the wild. One example I came across is DB UI Mono (the Deutsche Bahn design system). It seems to work for them, so take my conclusion with a grain of salt.

    TL;DR

    Mitosis is an open-source tool that aims to transform JSX components into functional components for multiple frameworks like React, Vue, and Angular. It offers benefits such as time savings and reduced need for framework-specific knowledge. However, it comes with challenges including a steep learning curve, strict coding rules, debugging difficulties, and lack of control over output. While promising, Mitosis isn't a perfect solution for cross-framework development yet and requires careful consideration before implementation in production environments.

    ¹ Web components have been trying to fill this void by providing a standard component model for the web allowing for encapsulation and interoperability, but come with their own limitations.

    ² Mitosis uses a *.lite.jsx naming convention. But since Mitosis isn’t that popular yet, my IDE validates the code as React (read JSX) code. You can configure TS to use Mitosis’ JSX types, which gets rid of some of these problems.

    ³ You can alter this behavior by using the powerful Mitosis plugins, but this definitely requires a lot more time and effort seeing as you are basically extending the compiler with your own logic.

    ⁴ Sami Jaber from the Mitosis team rightfully noted that you can use the onInit hook to achieve the same logging behavior in Mitosis. But that still leaves us with the annoying Mitosis ‘gotcha’ of no code written in the render body of a component being persisted (since this is a valid use case for defining constants/magic-numbers and so forth).

    Related blog posts

    ← All blog posts