Skip to main content

Cross-Framework Components with Mitosis

Build components once, compile to React, Vue, Svelte, Solid, and Qwik.

What is Mitosis?

Mitosis is a tool that lets you write components in a framework-agnostic syntax and compile them to any framework. AutoDeployBase uses Mitosis to create cross-framework plugins.

Getting Started

Write a Mitosis Component

components/Counter.lite.tsx
import { useState } from '@builder.io/mitosis';

export default function Counter(props: { initialCount?: number }) {
const [count, setCount] = useState(props.initialCount || 0);

return (
<div class="counter">
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
);
}

Compile

npm run mitosis:compile

Output

The component compiles to all supported frameworks:

output/
├── react/
│ └── Counter.tsx
├── vue/
│ └── Counter.vue
├── svelte/
│ └── Counter.svelte
├── solid/
│ └── Counter.tsx
└── qwik/
└── Counter.tsx

Mitosis Syntax

State

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

export default function MyComponent() {
const [name, setName] = useState('');
const [count, setCount] = useState(0);

return (
<div>
<input value={name} onChange={e => setName(e.target.value)} />
<p>Hello, {name}!</p>
</div>
);
}

Props

interface ButtonProps {
label: string;
variant?: 'primary' | 'secondary';
disabled?: boolean;
onClick?: () => void;
}

export default function Button(props: ButtonProps) {
return (
<button
class={`btn btn-${props.variant || 'primary'}`}
disabled={props.disabled}
onClick={() => props.onClick?.()}
>
{props.label}
</button>
);
}

Conditionals

export default function Greeting(props: { isLoggedIn: boolean; name: string }) {
return <div>{props.isLoggedIn ? <p>Welcome back, {props.name}!</p> : <p>Please log in</p>}</div>;
}

Loops

interface ListProps {
items: string[];
}

export default function List(props: ListProps) {
return (
<ul>
{props.items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
);
}

Event Handlers

export default function Form() {
const [value, setValue] = useState('');

function handleSubmit(e: Event) {
e.preventDefault();
console.log('Submitted:', value);
}

return (
<form onSubmit={e => handleSubmit(e)}>
<input value={value} onChange={e => setValue(e.target.value)} />
<button type="submit">Submit</button>
</form>
);
}

Lifecycle

import { onMount, onUnMount } from '@builder.io/mitosis';

export default function Timer() {
const [seconds, setSeconds] = useState(0);
let interval: number;

onMount(() => {
interval = setInterval(() => {
setSeconds(seconds + 1);
}, 1000);
});

onUnMount(() => {
clearInterval(interval);
});

return <p>Seconds: {seconds}</p>;
}

Refs

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

export default function FocusInput() {
const inputRef = useRef<HTMLInputElement>(null);

function focusInput() {
inputRef.current?.focus();
}

return (
<div>
<input ref={inputRef} />
<button onClick={() => focusInput()}>Focus</button>
</div>
);
}

Plugin Structure

Directory Layout

plugins/my-plugin/
├── manifest.json
├── generator.js
├── src/
│ └── components/
│ ├── Button.lite.tsx
│ ├── Card.lite.tsx
│ └── Modal.lite.tsx
├── output/ # Generated
│ ├── react/
│ ├── vue/
│ └── svelte/
└── mitosis.config.js

Mitosis Config

mitosis.config.js
module.exports = {
files: 'src/components/**/*.lite.tsx',
targets: ['react', 'vue', 'svelte', 'solid', 'qwik'],
dest: 'output',
options: {
react: {
typescript: true,
stylesType: 'style-tag'
},
vue: {
typescript: true,
api: 'composition'
},
svelte: {
typescript: true
}
}
};

Generator Integration

generator.js
import { sdk } from 'autodeploybase';

export default async function generate(context) {
const { framework, projectPath } = context;

// Map framework to Mitosis output
const frameworkMap = {
next: 'react',
nuxt: 'vue',
sveltekit: 'svelte',
remix: 'react',
astro: 'react',
react: 'react'
};

const targetFramework = frameworkMap[framework];

// Copy compiled components
await sdk.copyTemplates({
source: `./output/${targetFramework}`,
destination: `${projectPath}/src/components/my-plugin`
});
}

Best Practices

1. Keep Components Simple

Mitosis works best with simple, presentational components:

// Good - simple, presentational
export default function Avatar(props: { src: string; name: string }) {
return <img class="avatar" src={props.src} alt={props.name} />;
}

// Avoid - complex business logic
export default function UserDashboard() {
// Too complex for Mitosis
}

2. Use CSS Classes

Prefer class names over inline styles:

// Good
<div class="card card-primary">...</div>

// Avoid
<div style={{ padding: '1rem', border: '1px solid #ccc' }}>...</div>

3. Avoid Framework-Specific Features

Don't use React hooks, Vue composables, or Svelte stores:

// Good - framework agnostic
const [count, setCount] = useState(0);

// Avoid - React specific
const value = useMemo(() => expensive(), [dep]);

4. Use Primitive Props

Stick to primitive types for props:

// Good
interface Props {
title: string;
count: number;
isActive: boolean;
items: string[];
}

// Avoid - complex objects with methods
interface Props {
user: User;
onUpdate: (user: User) => Promise<void>;
}

5. Separate Logic from UI

Keep business logic outside Mitosis components:

// utils/counter.ts
export function calculateTotal(items: number[]): number {
return items.reduce((sum, item) => sum + item, 0);
}

// Counter.lite.tsx
import { calculateTotal } from '../utils/counter';

export default function Counter(props: { items: number[] }) {
const total = calculateTotal(props.items);
return <p>Total: {total}</p>;
}

Compilation Commands

# Compile all Mitosis components
npm run mitosis:compile

# Compile specific plugin
npm run mitosis:compile -- --plugin plugins/my-plugin

# Watch mode
npm run mitosis:compile -- --watch

# Verbose output
npm run mitosis:compile -- --verbose

Troubleshooting

Component Not Compiling

  1. Check file extension is .lite.tsx
  2. Verify export default function
  3. Check for unsupported syntax

Styles Not Applied

  1. Use class not className
  2. Include CSS file in generator
  3. Check CSS variable registration

Type Errors

  1. Install @builder.io/mitosis
  2. Add types to tsconfig
  3. Use interface for props

Example: Complete Plugin

src/components/DataTable.lite.tsx
import { useState } from '@builder.io/mitosis';

interface Column {
key: string;
label: string;
}

interface DataTableProps {
columns: Column[];
data: Record<string, any>[];
onRowClick?: (row: Record<string, any>) => void;
}

export default function DataTable(props: DataTableProps) {
const [sortColumn, setSortColumn] = useState('');
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');

function handleSort(column: string) {
if (sortColumn === column) {
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
} else {
setSortColumn(column);
setSortDirection('asc');
}
}

return (
<table class="data-table">
<thead>
<tr>
{props.columns.map(col => (
<th
key={col.key}
onClick={() => handleSort(col.key)}
class={sortColumn === col.key ? 'sorted' : ''}
>
{col.label}
{sortColumn === col.key && (
<span class="sort-indicator">{sortDirection === 'asc' ? '↑' : '↓'}</span>
)}
</th>
))}
</tr>
</thead>
<tbody>
{props.data.map((row, index) => (
<tr key={index} onClick={() => props.onRowClick?.(row)}>
{props.columns.map(col => (
<td key={col.key}>{row[col.key]}</td>
))}
</tr>
))}
</tbody>
</table>
);
}