Back to Articles
VueNuxtReactFrontend

Nuxt & Vue for the React Developer

A teaching blog for React/Next/TypeScript developers migrating to Vue and Nuxt. Part 1 puts the two stacks head to head — philosophy, strengths, trade-offs. Part 2 is a full course that teaches Vue and Nuxt through the React concepts you already know.

July 11, 202625 min read

Why This Blog Exists

I've spent my frontend career in the React world — React, Next.js, TypeScript, TanStack Query, Zustand, the whole ecosystem. Recently I started migrating my projects to Vue and Nuxt, and I'm learning it the way most React developers do: constantly asking "okay, but what's the Vue equivalent of X?"

This is a teaching blog. It has two main parts. Part 1 compares the two stacks head-on — philosophy, features, advantages, disadvantages — so you know what you're signing up for. Part 2 is the big one: a detailed course that teaches Vue and Nuxt through React and Next.js — every concept mapped to the one you already know.

If you know React, Next.js, and TypeScript, you already know about 80% of Vue and Nuxt — you just know it by different names. This post is the dictionary.

Part 1 — The Two Stacks, Head to Head

First, the ecosystem map. Almost every tool you use in the React world has a first-class counterpart in the Vue world — and notably, most of the Vue counterparts are official, maintained by the core team, rather than community picks:

ConcernReact WorldVue World
UI libraryReactVue 3
Meta-frameworkNext.jsNuxt 3/4
Routing (SPA)React Router / TanStack RouterVue Router (official)
Global stateRedux / Zustand / JotaiPinia (official)
Server data fetchingTanStack Query / SWR / RSCuseFetch & useAsyncData (built into Nuxt)
Component syntaxJSX / TSXSingle File Components (.vue)
Local stateuseState / useReducerref / reactive
Derived stateuseMemocomputed
Side effectsuseEffectwatch / watchEffect / lifecycle hooks
Reusable logicCustom hooksComposables
Dev toolingVite / TurbopackVite (default)

The deeper difference isn't the tools — it's the philosophy. Two things separate these stacks at the core:

1. Re-render vs. fine-grained reactivity. React's model: state changed, so re-run the whole component function and diff the output. Your function body executes on every render — which is why you need useMemo, useCallback, and React.memo to opt out of work. Vue's model: your component's setup code runs once. Vue wraps your state in proxies, tracks exactly which piece of the DOM depends on which piece of state, and surgically updates only that. Optimization is the default, not something you add.

2. Library vs. framework, à la carte vs. batteries-included. React ships a rendering library and lets the community fight over everything else. Vue ships the router, the state manager, and the docs to glue them. Nuxt goes further than Next — auto-imports, built-in data fetching with SSR-aware caching, a server engine (Nitro) that deploys anywhere, and an official module ecosystem for images, fonts, SEO, auth, and content.

React + Next.jsVue + Nuxt
StrengthsBiggest ecosystem and job market; RSC pushes rendering boundaries; enormous community, every problem already answered; JSX is just JavaScript — full language power in markupGentler learning curve; less boilerplate for the same result; performance by default (no memoization ceremony); official, cohesive tooling; SFCs keep template/logic/styles together; Nuxt auto-imports remove import noise
WeaknessesYou assemble (and maintain) the stack yourself; hooks rules are easy to violate; re-render debugging is a genre of its own; App Router / RSC split the ecosystem into two mental modelsSmaller ecosystem and job market; fewer niche libraries; template DSL (v-if, v-for) must be learned; .value on refs trips everyone at first; less common in large enterprises (though growing)
Choose it whenTeam already knows it; you need a niche library that's React-only; hiring pool matters more than DXYou want to ship fast with less code; solo projects and startups; you value convention over configuration
My honest take after switching: neither stack is "better" — but Vue/Nuxt is noticeably less work.The things React makes you earn (stable references, memoized selectors, careful effect dependencies), Vue simply doesn't charge for. The price is a smaller ecosystem and learning a template syntax. For my own projects, that trade is worth it.

Part 2 — Learning Vue & Nuxt as a React Developer

Everything below teaches Vue/Nuxt by mapping it onto what you already know. Each topic shows the React version first, then the Vue version, then the gotchas. Work through it in order — the concepts build on each other.

2.1 — Components: From TSX to Single File Components

In React, a component is a function that returns JSX. In Vue, a component is a .vue file with three blocks: <script setup> (your logic), <template> (your markup), and optional <style>. Same component, both worlds:

Things to notice, because they generalize:

React habitVue equivalentNote
{expression}{{ expression }}Double curlies in templates
onClick={fn}@click="fn"@ is shorthand for v-on:
setCount(count + 1)count++Mutation is fine — state is a proxy, Vue sees the write
CSS Modules / styled-components<style scoped>Scoping is built into the SFC
export default function<script setup>No export, no function wrapper — the file IS the component
The biggest unlearning: in Vue, mutating state is not a sin — it's the API. React needs immutability because it detects change by comparing references. Vue detects change by intercepting the write itself (via Proxy). count++, user.name = "new", list.push(item) — all perfectly idiomatic.

2.2 — Reactivity: ref, reactive, and the .value Gotcha

This is the heart of Vue, so let's be precise. Vue gives you two primitives:

ts
import { ref, reactive } from "vue";

// ref: for ANY value (primitives included).
// Access/write via .value in script code.
const count = ref(0);
count.value++;              // write
console.log(count.value);   // read

// reactive: for objects only. No .value — but you
// lose reactivity if you destructure or reassign it.
const state = reactive({ name: "Daksh", score: 9 });
state.score = 10;           // just mutate

Why does ref need .value? JavaScript proxies can only intercept property access on objects — you can't proxy a bare number. So Vue boxes your value in an object with a .value property it can watch. In templates, refs are auto-unwrapped — you write {{ count }}, never {{ count.value }}.

The #1 beginner bug: forgetting .value in script code (or adding it in templates). My rule of thumb from the React side: a ref is like a useRef whose .current is reactive — same shape, but writes trigger updates. Start with ref for everything; reach for reactiveonly for grouped state you'll never reassign.

Now the derived-state and side-effect mappings:

Read that again, because it's the single biggest quality-of-life difference: there are no dependency arrays in Vue.No exhaustive-deps lint rule, no stale closures, no "why is my effect firing twice." Vue records which reactive values a computed/watchEffect actually reads, and subscribes to exactly those. The entire class of bugs where you forgot a dependency — gone.

2.3 — Props, Events, and v-model

Props flow down in both frameworks. The difference is the upward direction: React passes callbacks down; Vue components emit events up. Fully typed, both directions:

vue — parent usage
<SearchBox
  placeholder="Search blogs..."
  @search="(q) => runSearch(q)"
/>

Three template mechanics worth pausing on:

:placeholder — the colon binds an attribute to a JS expression (shorthand for v-bind:). Without the colon it's a literal string, like HTML.

v-model — two-way binding. In React, every input is a controlled-component ceremony: value={query} onChange={(e) => setQuery(e.target.value)}. In Vue, v-model="query" does both. It also works on custom components — defineModel() gives you a two-way-bound prop in one line.

@keyup.enter — event modifiers. .enter, .prevent (preventDefault), .stop (stopPropagation), .once. The e.preventDefault(); if (e.key === "Enter") boilerplate is just... gone.

2.4 — Template Syntax: v-if, v-for, and Losing JSX

This is the part React developers resist most, so let me map it directly. JSX uses JavaScript for control flow; Vue templates use directives:

JSX patternVue directiveDifference
cond && <X />v-ifv-if actually unmounts; v-show just toggles CSS display
ternary chainsv-if / v-else-if / v-elseFlat, reads top to bottom
array.map()v-for="item in items":key is still required, same rules as React
Fragment <>...</>not neededVue 3 templates allow multiple root nodes
classNameclassIt's real HTML — class, for, tabindex as-is
:class object:class="{ active: isActive }"Built-in conditional classes — no clsx/classnames dependency

Two honest notes. First: yes, you're learning a DSL, and for the first week you'll miss "it's just JavaScript." Second: the DSL is whyVue can optimize so well — because templates are statically analyzable, the compiler pre-computes which parts of the DOM can ever change and skips the rest entirely. JSX's full flexibility is precisely what React's compiler struggles against. And if you truly need dynamic render logic, Vue supports JSX and render functions too — it's just rarely needed.

2.5 — Slots: children and Render Props, Solved Once

React's children prop is Vue's default slot. React's "multiple children sections" pattern (passing JSX via props) is named slots. Render props are scoped slots:

Same power as render props, but declarative and with less nesting. This pattern is everywhere in Vue UI libraries, so it's worth internalizing early.

2.6 — Lifecycle and Composables (Custom Hooks, Unshackled)

Lifecycle first — the mapping is nearly one-to-one:

ReactVueRuns
useEffect(fn, [])onMounted(fn)After the component is in the DOM
useEffect cleanup + []onUnmounted(fn)When the component is removed
useEffect(fn, [dep])watch(dep, fn)When specific state changes
useLayoutEffectwatchEffect w/ flush: 'post'After DOM updates
(no equivalent)onServerPrefetchSSR-only data hook

Custom hooks become composables — same idea (a function using reactive primitives, returning reactive state), one huge difference: the Rules of Hooks don't exist. Because setup runs once and reactivity lives in the values (not in call order), you can call composables conditionally, in loops, wherever:

2.7 — Global State: From Redux/Zustand to Pinia

Pinia is Vue's official store, and if you've used Zustand it will feel like coming home — minus the selector functions, because Vue's reactivity makes components auto-subscribe to exactly the fields they read:

Notice the store is literally a composable — the same ref/computed primitives you just learned, registered globally. No actions/reducers split, no immer, no useSelector memoization. Pinia also has devtools with time travel and full TypeScript inference out of the box.

2.8 — Nuxt for Next.js Developers

Now the meta-framework layer. Nuxt is to Vue what Next is to React, and the mapping is direct:

FeatureNext.js (App Router)Nuxt 3/4
Pagesapp/blog/[slug]/page.tsxpages/blog/[slug].vue
Layoutsapp/layout.tsx (nested)layouts/default.vue + <NuxtLayout>
API routesapp/api/x/route.tsserver/api/x.ts (Nitro)
Middlewaremiddleware.ts (edge)middleware/*.ts (route) + server middleware
Data fetchingRSC async components / fetchuseFetch / useAsyncData
Client-server boundary"use client" / "use server"No directive — .client/.server file suffixes when needed
Rendering modesSSR / SSG / ISR / PPRSSR / SSG / ISR / SPA — per-route via routeRules
Confignext.config.jsnuxt.config.ts
Env varsprocess.env.NEXT_PUBLIC_*runtimeConfig (server + public split)
Image optimizationnext/image@nuxt/image module

The two ideas that genuinely differ and deserve explanation:

1. Data fetching: useFetch. Next App Router splits your world into server components (fetch directly) and client components (TanStack Query et al). Nuxt has one primitive that does both jobs:

It's SSR-aware, deduplicated, cached by key, typed end-to-end (the return type of your server/api handler flows into data automatically), with refresh(), lazy variants, and reactive keys — refetch when the route param changes, no dependency array.

2. The server side: Nitro. Nuxt's server engine is a standalone thing. Any file in server/api/ becomes an endpoint:

ts — server/api/posts/[slug].ts
export default defineEventHandler(async (event) => {
  const slug = getRouterParam(event, "slug");
  const post = await db.post.findUnique({ where: { slug } });
  if (!post) throw createError({ statusCode: 404 });
  return post; // type flows into useFetch on the client
});

And auto-imports, the most controversial Nuxt feature: components in components/, composables in composables/, and all of Vue's primitives (ref, computed, watch) are available without import statements. Feels wrong for a day, then you stop missing the 15-line import blocks at the top of every Next page. It's fully typed — TypeScript knows exactly what's in scope.

2.9 — The Migration Gotchas List

Everything that actually bit me, so it doesn't bite you:

GotchaWhat happensFix
Forgetting .value in scriptComparisons/math silently wrong (ref object, not value)ESLint vue rules catch most; templates never need .value
Destructuring reactive()const { name } = state — name is a dead copyUse toRefs(state), or just use ref everywhere
Destructuring propsconst { label } = props loses reactivityAccess props.label, or use toRefs(props)
Expecting re-rendersconsole.log in setup runs ONCE, not per updateSetup is not a render function — put logs in watchEffect
key on v-for with indexSame pitfalls as React — state bleeds across itemsUse stable ids, same discipline as React
v-if with v-for on one elementPrecedence is confusingNest them, or filter in a computed first
SSR hydration mismatchSame class of bug as NextWrap browser-only markup in <ClientOnly>
The meta-gotcha: stop porting React patterns literally.You don't need useCallback-style stable references (nothing re-renders to break them). You don't need context for prop drilling (provide/inject exists, and Pinia is cheap). You don't need Suspense choreography for data (useFetch handles SSR). The instinct to reach for a workaround is usually the sign that Vue already solved it upstream.

Key Takeaway

If you know React and Next, you are two honest weekends away from productive Vue and Nuxt. The component model, one-way data flow, file-based routing, SSR mental model — it all transfers. What changes is the cost structure: Vue's fine-grained reactivity means the performance work React makes you do by hand (memoization, dependency arrays, selector tuning) simply doesn't exist, and Nuxt's conventions mean the stack decisions Next leaves to you (data fetching, state, imports) come pre-made and official.

Learn ref and .value, embrace mutation, let go of dependency arrays, and read your compiler errors — the rest is renaming things you already know.

Resources