React or Vue? The Stack I Standardize My Teams On (and Why it's Vue)
React or Vue? The Stack I Standardize My Teams On
This is not an argument that React is bad.
React is good. React is proven. React has a huge hiring pool, a giant ecosystem, and a lot of excellent tooling.
But when I reach for Vue, especially with Quasar, Pinia, TanStack Vue Query, Tailwind, and often PocketBase / Meilisearch behind it, I’m usually choosing a stack that feels easier to teach, easier to standardize, and easier to keep boring.
These days I make that call as a lead architect, for a team and not just for myself, and that changes what “good” means. I optimize less for what I can build alone and more for what the whole team can learn, ship, and keep maintaining after I’ve moved on to the next thing.
My gut feeling is:
The fewer decisions a team has to make before shipping, the faster everyone moves and the easier the codebase is to hand off.
The leadership-ey reason: fewer decisions before shipping
React often feels like a powerful rendering library surrounded by choices.
A React project may need to decide:
Which app framework?
Which router?
Which UI kit?
Which state library?
Which query/cache library?
Which form library?
Which table library?
Which mobile wrapper?
Which project conventions?
Those choices are not bad. They’re pretty normal for many projects and are part of React’s strengths.
But they are still choices and decisions.
With my current Vue stack, the shape is more intuitive I think:
Quasar = UI/app/platform framework including Vue components
This is a lot like the UI kits like Material UI and Tailwind Plus but super charged
Compile to any platform - SPA/PWA/SSR/Capacitor/Cordova/Electron/Browser extensions too
Pinia = client/app/domain state
Pinia-plugin-persisted-state is a game changer for local persistence - https://www.npmjs.com/package/pinia-plugin-persistedstate
Tailwind = Primary styling after Quasar component props
Quasar has great built in style/css features but it’s not my style - so I found Tailwind with a prefixer if needed works great to pivot away from the default Quasar aesthetic
Optional and Stack Independent:
TanStack Vue Query = server/cache state (TanStack is on every framework I think)
PocketBase = lightweight backend/auth/content/config
Meilisearch = fast search/index layer
This setup, I strongly believe, gives the team a super low friction default. I’ve set up several projects this way and the velocity is outstanding.
Vue is legible to the layman developer
Vue components often look like what they do.
<template>
<q-card>
<q-card-section>
<div class="text-h6">{{ product.name }}</div>
<div class="text-subtitle2">{{ product.sku }}</div>
</q-card-section>
<q-card-actions align="right">
<q-btn label="Edit" color="primary" @click="$emit('edit', product)" />
</q-card-actions>
</q-card>
</template>
<script setup>
defineProps({
product: {
type: Object,
required: true,
},
})
defineEmits(['edit'])
</script>Most people with basic HTML and JavaScript can infer what is happening:
{{ value }} = print this value
@click = handle click event
:prop = bind a dynamic prop
defineProps = data coming in
defineEmits = events going outReact can also be readable, especially with a good team and conventions. But Vue templates feel closer to HTML and closer to the final UI structure.
That matters A LOT when onboarding people into an existing project.
Named slots are a great example
Vue named slots are one of the clearest examples of why Vue ‘feels’ right, and teachable, for me.
<ProductShell>
<template #title>
Edit Product
</template>
<ProductFields />
<template #actions="{ save, loading }">
<q-btn
label="Save"
color="primary"
:loading="loading"
@click="save"
/>
</template>
</ProductShell>This reads almost like instructions:
Put this in the title area.
Put this in the body.
Put this in the actions area.
The shell gives the actions slot save/loading.
React can solve the same problem with render props, children functions, or component props. But Vue’s syntax makes component extension points very obvious.
For internal component libraries, admin screens, and shared team UI patterns, that clarity is valuable. For my preferred topologies like app-shell setups, this is nearly essential lest we lose our minds in merge conflicts.
Pinia is boring in the best way
Pinia is easy to explain:
export const useUiStore = defineStore('ui', {
state: () => ({
selectedStoreView: 'default',
drawerOpen: false,
denseMode: true,
}),
actions: {
setStoreView(code) {
this.selectedStoreView = code
},
toggleDrawer() {
this.drawerOpen = !this.drawerOpen
},
},
persist: {
paths: ['selectedStoreView', 'denseMode'],
},
})The mental model is simple:
state = data
getters = computed values
actions = methods (aka queries, mutations)
persist = remember this (Pinia persisted state plugin is this)
And inside actions, you can usually just change the thing:
this.drawerOpen = !this.drawerOpenReact’s state model is powerful, but it often introduces more concepts earlier: immutability, reducers, providers, context, custom hooks, or third-party state libraries.
Not impossible. Just more ceremony.
Pinia should not become the cache layer
This is where TanStack Vue Query makes the Vue stack stronger.
Pinia = client/app/domain state | Vue Query = remote/server/cache state |
|---|---|
Pinia should remember things like:
| Vue Query should handle things like:
|
Example:
export function useProductQuery(sku) {
const ui = useUiStore()
return useQuery({
queryKey: computed(() => [
'product',
sku.value,
ui.selectedStoreView,
]),
queryFn: () => fetchProductBySku({
sku: sku.value,
storeView: ui.selectedStoreView,
}),
enabled: computed(() => !!sku.value),
staleTime: 1000 * 60 * 5,
})
}That keeps Pinia from becoming this:
state: () => ({
products: [],
productsFetchedAt: null,
productsLoading: false,
productErrors: {},
categoryProducts: {},
categoryProductsFetchedAt: {},
ttlByEndpoint: {},
})That’s not really app state anymore. That is a homemade query cache. I totally don’t do this all the time.
So the better rule is:
Pinia remembers what the user or app chose. Vue Query handles what the server said.
Quasar is a force multiplier
Quasar is probably the biggest practical reason I like Vue.
It gives me a standard way to build (That is not uncommon in other frameworks either):
layouts
drawers
menus
dialogs
forms
tables
notifications
loading states
dark mode support throughout
PWA apps
Capacitor apps
SSR-capable apps
desktop-style admin interfaces (Key combo menus and such are a breeze)
Instead of picking a separate package for every UI problem, the team can learn the Quasar way once and reuse it everywhere.
<q-table
:rows="rows"
:columns="columns"
row-key="sku"
:loading="loading"
>
<template #body-cell-actions="{ row }">
<q-td>
<q-btn
dense
flat
icon="edit"
@click="editRow(row)"
/>
</q-td>
</template>
</q-table>That is a big advantage for:
admin apps
internal tools
dashboards
ecommerce widgets
Magento (Or your platform)-adjacent SPAs
progressively mounted frontend components
mobile-ish PWA tools
Is there a React equivalent to Quasar?
Kind of, but not perfectly.
The React world has equivalents for pieces of Quasar:
UI component kit:
MUI, Mantine, Ant Design
Mobile/PWA/Capacitor path:
Ionic React + Capacitor
Admin/internal tools:
React Admin, Refine
Full-stack web framework:
Next.js
Native mobile:
Expo / React Native
A React version of a Quasar-ish stack might look like:
Vite or Next.js (Quasar uses Vite too)
+ Mantine or MUI
+ TanStack Query
+ Zustand or Redux Toolkit
+ React Hook Form
+ Capacitor if needed
For mobile-first apps:
Ionic React
+ Capacitor
+ TanStack Query
+ Zustand
For CRUD/internal business apps:
React Admin or Refine
+ MUI/Mantine/shadcn
+ TanStack Query
So React absolutely can do the same jobs.
The difference is that React usually assembles the stack from more separate decisions.
Vue/Quasar feels more integrated to me.
Quasar gives me the app shell, UI components, layout conventions, and platform targets.
React gives me excellent ingredients, but I still have to choose the recipe.
PocketBase and Meilisearch make the stack fun
A lot of my projects do not need to become enterprise platforms on day one.
They need to be useful, fast, understandable, and easy to change.
That is where this pattern feels nice:
PocketBase = auth, data, content/config, admin UI
Meilisearch = fast search, filters, projected read models, vectors, dynamic schemas (It’s Algolia)
Vue Query = fetch/cache remote state (TTL boilerplate be gone)
Pinia = local app state
Quasar = UI shell and Vue component model
A common architecture can be:
PocketBase stores source/config/content.
Hooks or workflows project search-friendly records.
Meilisearch serves fast indexed reads.
Vue Query fetches and caches remote data.
Pinia tracks local app decisions.
Quasar renders the interface.
That stack is not trying to impress anyone. It is trying to stay understandable while agile.
So then, why Vue
A React developer could fairly say:
React is easy too, once you know the patterns.
And they’re right! I’m not trying to talk anyone out of React.
My real reason is simpler, and a bit selfish as the person who owns the architecture:
I can hand this Vue + Quasar stack to a newer engineer and have them genuinely productive in under a week.
A lot of the architecture lives in the framework’s conventions, so there’s less “learn our project’s particular setup” stacked on top of “learn the language.” That is the difference between a new hire contributing this sprint and contributing next month.
I’m not experienced enough with React to claim you can’t get there too. I’d bet you can. I just know I don’t want to be the one assembling and re-teaching that recipe on every project, for every new person who joins.
So the takeaway isn’t that one beats the other:
React gives more freedom.
Vue with Quasar gives more defaults. Fewer wheels to invent or re-implement.
Freedom is great for some teams. Defaults are great for others. For the kind of teams I lead, defaults win more often.
Which comes down to one metaphor:
React gives me great ingredients. Quasar + Vue gives a kitchen, and a kitchen is a lot easier to share with a team.