“Write Components Once, Compile to Every Framework” With Mitosis

What I learned while creating a GitHub Activity Calendar with Mitosis, which generates React, Vue, Svelte, and other components

Patrick Chiu
Better Programming

--

All images and gifs by author

TL;DR Here is the Git repo and npm package you can used instantly with npm i activity-calendar-widget. See more for usage with React (doc | demo), Vue (doc | demo), Svelte (doc | demo), Solid (doc | demo) and Qwik (doc | demo).

Preface

Recently, I’ve been researching how to create a UI library that can be used in various modern frontend frameworks, e.g., React, Vue, and Svelte. I came across Mitosis from Builder.io and tried to create a complex enough GitHub Activity Calendar widget as a POC. Though there are some hiccups, overall, it is a good experience. I want to take this chance to journal the process and my learnings. Why create a calendar widget? Because it helps to demonstrate some key functionalities:

  1. Able to accept inputs (Parent to Child), e.g., passing data and render the activity stat accordingly
  2. Able to execute callbacks on events (Child to Parent), e.g., onClick and notify the parent which day box is being clicked
  3. Able to customize the theme, e.g., customize the date box from green to purple by CSS variables instead of inline passing a color props

Building a *Simplified* Activity Calendar

1. Outline the design

There are much more components and logic in the published components. However, to simplify this walkthrough, we will only create two main Mitosis components <Day /> and <ActivityCalendar />.

<ActivityCalendar /> is our entry point, which accepts a data array prop specifying each day’s activities. Regardless of whether there are activities, a <Day /> component will be rendered for each day.

.
├── preview
├── src/
│ ├── ActivityCalendar.lite.jsx
│ └── Day.lite.jsx
├── mitosis.config.js
└── package.json

The src directory stores our Mitosis components.

The output directory stores the generated components of the target frameworks. It can be customized in mitosis.config.js.
The mitosis.config.js file will be explained in step 3.

2. Initiate NPM and Install Mitosis

npm init -y
npm install @builder.io/mitosis-cli @builder.io/mitosis

3. Set up mitosis.config.js

/** @type {import('@builder.io/mitosis').MitosisConfig} */
module.exports = {
files: 'src/**',
targets: ['vue3', 'solid', 'svelte', 'react', 'qwik'],
dest: 'preview'
};

This is the Mitosis config JS file (You don’t say 😅). We can edit targets to add/remove framework components to generate, e.g., preact. Here’s Mitosis configuration doc, which contains all the available frameworks but does notice that some frameworks are in a very early stage (in fact, Mitosis itself is still in beta).

4. Set up the dev server?

Sadly, Mitosis doesn’t come with a Dev Server yet. In this walkthrough, you can try it out in Replit (edit the Mitosis components and generate native components) and preview the generated (e.g., React.js) components in Stackblitz.

In my local environment, I set up Astro to render multiple generated components on the same page. I will leave this to the next article.

5. <Day /> component

Here is the <Day /> source code.


export default function Day(props) {
return (
<div
onClick={() => props.handleDayClick(props.dt)}
style={{
position: 'absolute',
width: '10px',
height: '10px',
backgroundColor: 'var(--day-box-color, #39d353)',
borderRadius: '2px',
top: props.top,
right: props.right,
opacity: props.opacity,
}}
></div>
);
}

<Day /> is a simple component that renders a rounded corner square 🟩, which is the day box in the activity calendar. Its position is determined by props.top and props.right. While the box color (how light the box is) is determined by props.opacity. When clicked, the props.handleDayClick function will be called with props.dt, which contains the box’s date and time data.

6. <ActivityCalendar /> component

Here is the <ActivityCalendar /> source code.

import { For } from '@builder.io/mitosis';

import Day from './Day.lite';

const getDays = (props) => {
// Generate 180 days from today
// Merge with the activities of specified days
const dataObj = (props.data || []).reduce((acc, cur) => {
return {
...acc,
[cur.date]: cur.activities,
};
}, {});

return new Array(180).fill(0).map((_, index) => {
const dt = new Date();
dt.setDate(dt.getDate() - index);
const year = dt.getFullYear();
const month = dt.getMonth() + 1;
const day = dt.getDate();
const id = year + '-' + month + '-' + day;

return {
id,
year,
month,
day,
dayOfWeek: dt.getDay(),
dayDiffFromToday: index,
activities: dataObj[id] || [],
};
});
};

const getDayRight = ({
dayDiffFromToday = 0,
dayOfWeek = 0,
}) => {
const todayDayOfWeek = new Date().getDay();

return (
(Math.floor(dayDiffFromToday / 7) +
(dayOfWeek > todayDayOfWeek ? 1 : 0)) *
14
);
};

export default function ActivityCalendar(props) {
return (
<div
style={{
position: 'relative',
display: 'inline-block',
width: Math.ceil(180 / 7) * 14 + 'px',
height: 7 * 14 + 'px',
}}
>
<For
each={getDays({
data: props.data ?? [],
weekStart: props.weekStart,
daysToRender: props.daysToRender,
})}
>
{(dt, index) => (
<Day
key={index}
handleDayClick={props.handleDayClick}
dt={dt}
opacity={(dt.activities.length + 1) / 10 + ''}
top={dt.dayOfWeek * 14 + 'px'}
right={
getDayRight({
dayDiffFromToday: dt.dayDiffFromToday,
dayOfWeek: dt.dayOfWeek,
}) + 'px'
}
/>
)}
</For>
</div>
);
}

The getDays function generates an array with 180-day objects (180 is an arbitrary hard-coded value). If there are activities on the specified date in props.data, it will be merged to that specified day object. Regardless of whether there are activities, there will be a day object for each date, which will then render the <Day />s.

In the <ActivityCalendar /> component, <For each={getDays()}> … </For>, which is a Mitosis API, is used to render each of the <Day />s. The props that <Day /> needs are passed in a very similar React.js fashion.

Here is what props.data looks like. There are activities on 2023–4–1 and 2023–3–26:

[
{
"date": "2023-4-1",
"activities": [
{ "title": "commit code" },
{ "title": "review pr" },
{ "title": "open issue" },
{ "title": "commit code" }
],
},
{
"date": "2023-3-26",
"activities": [{}, {}, {}, {}, {}, {}, {}, {}],
}
];

7. Generate the target components!

Now, the exciting moment! Let’s generate the target components by using the npm exec mitosis build command. Since we specify our targets frameworks as ['react', 'svelte', 'vue3', 'solid', 'qwik] in mitosis.config.js. The following screenshot is what we can expect:

8. See what it looks like in Vue and try out the onClick handler handleDayClick

<template>
<div
:style="{
position: 'relative',
display: 'inline-block',
width: Math.ceil(180 / 7) * 14 + 'px',
height: 7 * 14 + 'px',
}"
>
<template
:key="index"
v-for="(dt, index) in getDays({
data: data ?? [],
weekStart: weekStart,
daysToRender: daysToRender,
})"
>
<day
:handleDayClick="handleDayClick"
:dt="dt"
:opacity="(dt.activities.length + 1) / 10 + ''"
:top="dt.dayOfWeek * 14 + 'px'"
:right="
getDayRight({
dayDiffFromToday: dt.dayDiffFromToday,
dayOfWeek: dt.dayOfWeek,
}) + 'px'
"
></day>
</template>
</div>
</template>

<script>
import Day from "./Day.vue";
const getDays = (props) => {
const dataObj = (props.data || []).reduce((acc, cur) => {
return {
...acc,
[cur.date]: cur.activities,
};
}, {});
return new Array(180).fill(0).map((_, index) => {
const dt = new Date();
dt.setDate(dt.getDate() - index);
const year = dt.getFullYear();
const month = dt.getMonth() + 1;
const day = dt.getDate();
const id = year + "-" + month + "-" + day;
return {
id,
year,
month,
day,
dayOfWeek: dt.getDay(),
dayDiffFromToday: index,
activities: dataObj[id] || [],
};
});
};
const getDayRight = ({ dayDiffFromToday = 0, dayOfWeek = 0 }) => {
const todayDayOfWeek = new Date().getDay();
return (
(Math.floor(dayDiffFromToday / 7) + (dayOfWeek > todayDayOfWeek ? 1 : 0)) *
14
);
};

export default {
name: "activity-calendar",
components: { Day: Day },
props: ["data", "weekStart", "daysToRender", "handleDayClick"],

methods: { getDays, getDayRight },
};
</script>

… And React, Svelte, Solid and Qwik. Not bad, huh? Let’s try passing a onClick handler called handleDayClick.

<ActivityCalendar
:daysToRender="150"
:data="data"
:handleDayClick="handleDayClick"
/>

<script>
export default {
name: 'App',
data() {
return {
data: [ ... ]
};
},
methods: {
handleDayClick: (dt) => {
console.log(dt);
},
},
};
</script>

Click the day box and see the result!

9. Customize the color theme

Lastly, let’s customize the box color by CSS variables instead of inline passing the color props. Indeed, theming by CSS variables is only one of the ways, and every framework has its own native way of doing theming. Mitosis might not be able to support the specific native theming, but it is good to know that Mitosis can them by CSS variables.

In another React.js demo, let’s try to add the following line to src/style.css.

:root {
--day-box-color: teal;
}

And here’s what the teal-ish Activity Widget looks like!

What I Learned

You might need to code in a “Mitosis” way

Mitosis is great and works fine most of the time, but it does come with limitations that restrict us from writing in a more declarative “Mitosis” way. Sometimes, I code something in a common React way, e.g., computing the default state value with props and finding out that it is not allowed. In addition, their ES Lint plugin is a great “reminder” of these constraints. Altogether, it should catch most of the errors.

Try extracting the logic into framework-agnostic pure functions

The restrictions are significantly lower if the logic isn’t placed inside a Mitosis functional component and is (mostly) pure. For instance, I’m allowed to destructure objects (Mitosis seems to hate destructuring 🫠 jk jk) and have comments inside, like how I normally did in React.js. Also, when I inspect the generated components of React.js or other frameworks, those functions are almost a 1-to-1 match.

Use ‘css’ for static styling and ‘style’ for dynamic styling

While css is the suggested way of styling, it seems impossible to use variables there. There is another way - the style props - of styling an element in Mitosis. If the styling is static (won’t be changed corresponding to variables), use css which will generate a CSS class or styled component in the target framework components.

Use style if you need to determine the styling value by variable, e.g., the opacity of the <Day /> box 🟩, which will generate an inline style attribute to the HTML element.

You can also place a CSS class to the component and provide CSS file(s) for the users to import. In addition, you can useStyle to style the component, which I haven’t played around with this option yet.

Avoid naming the function props as event listeners like on”Event,” e.g., onClick

When naming the function props as event listeners like on"Event", e.g., onClick and generating components for Svelte, it will be transformed to on:click. How about naming it as non-standard events like onDayClick? Currently, it is also generated to on:dayClick. To avoid this issue, try something like handleDayClick at this moment.

Mitosis doesn’t come with a dev server yet

While you can generate framework components by npm exec mitosis build, Mitosis doesn’t come with a dev server to preview them. I end up setting up a dev server with Astro. Not optimal, but usable enough. I will share the setup in the next article.

Epilogue

Overall, the experience of using Mitosis is similar to React.js, with a few hiccups. For example, I must constantly check whether the generated components break in certain frameworks. However, once I got used to the limitations, I would say Mitosis is a great tool to work with, especially when you want to build slightly more complex components than just a themed button or nav bar, which targets multiple frameworks.

I noticed that the communication between different frameworks and Web Components is improving nowadays, except React.js. Building a POC with Web Components is definitely on my radar. Follow and connect with me for more interesting projects!

Want to Connect?

Visit me at LinkedIn | GitHub | Twitter

--

--

https://patrick-kw-chiu.github.io/ Full stack dev from HK🇭🇰 | Cache Cloud | mongo-http.js | Create tools that may or may not be useful