All GuidesLast updated: April 2026
Migration Guide

Converting React Components to Vue 3 — MUI/Chakra to Quasar Migration

Last updated: April 2026 · 14 min read

Template Syntax: JSX vs Vue Single-File Components

The most fundamental difference between React and Vue is how you express your UI. React uses JSX, a JavaScript extension that lets you write HTML-like syntax directly in your component functions. Vue uses Single-File Components (SFCs) with a <template> block for markup, a <script setup> block for logic, and an optional <style> block for scoped CSS.

Event handling. React uses camelCase synthetic events like onClick, onChange, and onSubmit. Vue uses the @ shorthand for v-on: @click, @input, @submit. Vue also provides event modifiers like @click.prevent and @submit.stop that eliminate the need for e.preventDefault() calls.

Two-way binding. React requires you to wire up value and onChange separately for every form input. Vue’s v-model directive handles both in a single attribute. This is especially powerful with Quasar components, where v-model works consistently across inputs, selects, toggles, date pickers, and dialogs.

Conditional rendering. React uses ternary operators and logical && for conditional rendering inline. Vue uses the v-if, v-else-if, and v-else directives, which read more like natural language and are easier to follow in complex conditional trees. For list rendering, React uses .map() while Vue uses v-for.

Component Mapping: MUI to Quasar

Quasar is the most comprehensive Vue 3 component framework, offering 70+ components that cover the same ground as Material UI. Below are the most common component mappings. Use the FrontFamily Converter to automate these conversions.

Button → q-btn

React (MUI)
<Button variant="contained" color="primary"
  onClick={handleSave}>
  Save Changes
</Button>
Vue 3 (Quasar)
<q-btn color="primary" label="Save Changes"
  @click="handleSave"
/>

TextField → q-input

React (MUI)
<TextField
  label="Email"
  variant="outlined"
  value={email}
  onChange={(e) => setEmail(e.target.value)}
  fullWidth
/>
Vue 3 (Quasar)
<q-input
  label="Email"
  outlined
  v-model="email"
  class="full-width"
/>

Card → q-card

React (MUI)
<Card elevation={2}>
  <CardContent>
    <Typography variant="h6">Title</Typography>
    <Typography variant="body2">Content</Typography>
  </CardContent>
</Card>
Vue 3 (Quasar)
<q-card>
  <q-card-section>
    <div class="text-h6">Title</div>
    <div class="text-body2">Content</div>
  </q-card-section>
</q-card>

Dialog → q-dialog

React (MUI)
<Dialog open={isOpen} onClose={handleClose}>
  <DialogTitle>Confirm</DialogTitle>
  <DialogContent>
    Are you sure?
  </DialogContent>
  <DialogActions>
    <Button onClick={handleClose}>Cancel</Button>
  </DialogActions>
</Dialog>
Vue 3 (Quasar)
<q-dialog v-model="isOpen">
  <q-card>
    <q-card-section class="text-h6">
      Confirm
    </q-card-section>
    <q-card-section>
      Are you sure?
    </q-card-section>
    <q-card-actions align="right">
      <q-btn flat label="Cancel" v-close-popup />
    </q-card-actions>
  </q-card>
</q-dialog>

Select → q-select

React (MUI)
<Select
  value={role}
  onChange={(e) => setRole(e.target.value)}
  label="Role">
  <MenuItem value="admin">Admin</MenuItem>
  <MenuItem value="user">User</MenuItem>
</Select>
Vue 3 (Quasar)
<q-select
  v-model="role"
  label="Role"
  :options="[
    { label: 'Admin', value: 'admin' },
    { label: 'User', value: 'user' }
  ]"
/>

Switch → q-toggle

React (MUI)
<Switch
  checked={enabled}
  onChange={(e) => setEnabled(e.target.checked)}
/>
Vue 3 (Quasar)
<q-toggle v-model="enabled" />

State Management: React Hooks to Composition API

React and Vue 3 have converged on a similar mental model for state and side effects, but the APIs differ significantly. Understanding these mappings is critical for a successful migration.

useState → ref / reactive

React (MUI)
import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(c => c + 1)}>
      Count: {count}
    </button>
  );
}
Vue 3 (Quasar)
<script setup>
import { ref } from 'vue';

const count = ref(0);
</script>

<template>
  <button @click="count++">
    Count: {{ count }}
  </button>
</template>

useEffect → onMounted / watch

React (MUI)
useEffect(() => {
  fetchUser(userId).then(setUser);
}, [userId]);

useEffect(() => {
  document.title = user?.name ?? '';
  return () => {
    document.title = 'App';
  };
}, [user]);
Vue 3 (Quasar)
import { watch, onMounted, onUnmounted } from 'vue';

onMounted(() => {
  fetchUser(userId.value).then(u => user.value = u);
});

watch(userId, (newId) => {
  fetchUser(newId).then(u => user.value = u);
});

watch(user, (u) => {
  document.title = u?.name ?? '';
});

useMemo → computed

React (MUI)
const fullName = useMemo(
  () => `${firstName} ${lastName}`,
  [firstName, lastName]
);
Vue 3 (Quasar)
const fullName = computed(
  () => `${firstName.value} ${lastName.value}`
);
// No dependency array needed — Vue tracks
// reactive dependencies automatically.

Key Differences to Watch For

Beyond component and API mappings, there are architectural differences between React and Vue that affect how you structure your migrated application:

ConceptReactVue 3
ReactivityImmutable state, re-renders entire treeProxy-based, fine-grained updates
Slots / Childrenchildren prop<slot> with named slots
Context / ProvidecreateContext + useContextprovide + inject
Refs (DOM)useRefref + template ref
CSS scopingCSS Modules or CSS-in-JS<style scoped> built-in
Form bindingvalue + onChangev-model (two-way)
RouterReact Router / Next.jsVue Router / Nuxt

Interactive Component Reference

Search any React MUI component to find its Vue Quasar equivalent. Components marked with ⚠ have no direct replacement and require custom implementation.

24 / 24
React / MUIVue / QuasarProps / Notes
Button
variant, color, disabled, onClick, startIcon
q-btn
color, disable, @click, icon, label
No variant prop; use flat, outline, unelevated props instead. Label replaces children text
TextField
label, variant, value, onChange, fullWidth
q-input
label, outlined/filled, v-model, class="full-width"
v-model replaces value+onChange pair; variant becomes boolean props (outlined, filled)
Card
elevation, CardContent, CardHeader, CardActions
q-card
q-card-section, q-card-actions
Uses q-card-section for content areas; elevation via CSS class or flat prop
Typography
variant, gutterBottom, color
div / span
class="text-h6", class="text-body2"
No Typography component; use Quasar CSS typography classes on native elements
Dialog
open, onClose, maxWidth
q-dialog
v-model, persistent, maximized
v-model controls open state; uses q-card inside for content structure
Chip
label, color, variant, onDelete
q-badge / q-chip
label, color, outline, removable
q-chip for interactive chips; q-badge for simple labels
Avatar
src, alt, sx
q-avatar
icon, color, text-color, size
Use q-img inside q-avatar for image avatars; supports icon prop directly
Switch
checked, onChange, color
q-toggle
v-model, color, label
v-model replaces checked+onChange; has built-in label prop
Alert
severity, variant, onClose
q-banner
class, inline-actions, dense
q-banner is simpler; use with custom classes for severity colors
Select
value, onChange, label, MenuItem children
q-select
v-model, label, :options, emit-value
Uses options array instead of children; v-model for two-way binding
CircularProgress
size, color, variant
q-spinner
size, color
Multiple spinner variants available: q-spinner-dots, q-spinner-bars, etc.
LinearProgress
value, variant, color
q-linear-progress
value, color, stripe, indeterminate
stripe prop instead of variant; indeterminate is a boolean prop
Tooltip
title, placement, arrow
q-tooltip
anchor, self, offset
Uses anchor/self position strings instead of placement; wraps target element
Divider
orientation, variant
q-separator
vertical, inset, spaced
orientation="vertical" becomes vertical prop; supports inset and spacing
Tabs
value, onChange, variant
q-tabs
v-model, align, active-color
Uses q-tab children and q-tab-panels + q-tab-panel for content
DataGrid
rows, columns, pageSize, sortModel
q-table
:rows, :columns, :pagination, :filter
q-table has built-in search, sorting, pagination, and selection
Autocomplete
options, renderInput, onChange
q-select (with filter)
v-model, :options, use-input, @filter
q-select with use-input and @filter event provides autocomplete behavior
Drawer
open, onClose, anchor
q-drawer
v-model, side, bordered
side="left"/"right" replaces anchor; typically used with q-layout
Menu
anchorEl, open, onClose
q-menu
v-model, anchor, self
Attach to parent element; no anchorEl ref needed. Uses v-model for open state
Snackbar
open, autoHideDuration, message
$q.notify()
message, timeout, type, position
Called via Quasar plugin: $q.notify({ message, type }); no component needed
Stepper
activeStep, orientation
q-stepper
v-model, vertical, animated
Uses q-step children with name, title, icon props
Upload
N/A (no MUI built-in)
q-uploader
url, auto-upload, multiple, accept
Built-in file uploader with drag-and-drop, progress, and file list
TreeView
items, onNodeSelect
q-tree
:nodes, node-key, selected
Quasar tree has built-in selection, expansion, and lazy loading
DatePicker
value, onChange (@mui/x)
q-date
v-model, mask, :options
Built-in rich date picker; supports range selection and custom disabled dates

What Developers Actually Hit

Based on migration reports from engineering teams. These are the problems documentation doesn’t warn you about.

⚠ Reactivity model is fundamentally different

React re-renders the entire component on state change. Vue uses a proxy-based reactivity system that only updates what changed. Code that relies on React’s render-cycle behavior (useEffect cleanup, ref callbacks, render-phase side effects) has no Vue equivalent — it needs architectural rethinking. A React component that resets local variables on every render silently breaks when ported to Vue because Vue’s setup() only runs once.

⚠ Slot system vs children prop

React passes children as props. Vue uses named slots with <slot name="header" />. Components with complex children composition (render props, function-as-children patterns) need complete restructuring for Vue’s slot system. A React pattern like <DataTable renderRow={(row) => ...} /> becomes a scoped slot in Vue, which is a different mental model entirely.

⚠ Event payloads differ despite similar naming

React uses onChange, onClick, onSubmit. Vue uses @change, @click, @submit. But the event payloads differ — React’s onChange on an input gives you a SyntheticEvent (access value via e.target.value), while Vue’s @input gives you the raw value directly. Teams doing automated find-and-replace miss this payload difference, causing TypeError: Cannot read property 'target' of undefined at runtime.

⚠ No React Context equivalent in Quasar

React Context is used for theme, auth, and feature flag providers. Vue has provide/inject but it’s less commonly used for global state. Quasar has its own plugin system with $q globals. Teams that relied heavily on nested React Context providers need to redesign their state architecture — either adopting Pinia for global state or restructuring to use Vue’s provide/inject with reactive refs.

Convert React to Vue instantly

Skip the manual rewrite. Paste your React MUI code into the FrontFamily Converter and get production-ready Vue 3 Quasar output with correct template syntax, v-model bindings, and Composition API setup. A pre-loaded Card example is ready for you to try.

Open Converter with React → Vue Example

Migration Strategy for Large Codebases

Micro-frontend approach. Unlike migrating between two React component libraries, switching from React to Vue requires running two entirely different frameworks. The most practical approach is a micro-frontend architecture where new features are built in Vue while existing React code continues to run alongside. Tools like Module Federation, single-spa, or even simple iframe embedding (as FrontFamily itself uses) allow both frameworks to coexist in production.

Start with leaf pages. Identify pages or features with minimal shared state. Settings pages, documentation views, and standalone dashboards make excellent migration targets because they have limited inter-component communication. Once your team builds confidence with Vue patterns, tackle the more interconnected features.

Shared state layer. If both React and Vue components need access to the same state, extract your state management into a framework-agnostic layer. A vanilla TypeScript store with event emitters, or a library like Zustand (which works outside React), can serve as a bridge. Both React hooks and Vue composables can subscribe to the same underlying store, ensuring data consistency during the migration period.

Import Changes at a Glance

React (MUI)
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import { Card, CardContent } from '@mui/material';
import { Dialog } from '@mui/material';
import { Select, MenuItem } from '@mui/material';
Vue 3 (Quasar)
<!-- Quasar components are globally registered -->
<!-- No imports needed in <template> -->

<script setup>
// Only import composables and utilities
import { useQuasar } from 'quasar';
import { ref, computed } from 'vue';
</script>