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
- Check file extension is
.lite.tsx - Verify export default function
- Check for unsupported syntax
Styles Not Applied
- Use
classnotclassName - Include CSS file in generator
- Check CSS variable registration
Type Errors
- Install
@builder.io/mitosis - Add types to tsconfig
- 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>
);
}