This content originally appeared on DEV Community and was authored by Graeme George
Welcome to a personal journey as I move from years of React and Next.js into the world of Vue 3 and Nuxt. It’s not meant to be a polished “final guide,” but rather a living, evolving document — part learning journal, part reference, and part cheat-sheet.
The focus is on mapping familiar React and Next.js patterns to their Vue and Nuxt counterparts, with plenty of code snippets and “mini-labs” along the way.
The tone here is intentionally hands-on and pragmatic. It’s written for experienced engineers who don’t need the basics explained, but want a direct path to becoming productive in Vue while still acknowledging that this is a journey, and the document will grow and change as we learn more.
If you're a seasoned Vue engineer, please do feel free to leave a comment and tell me where I'm going wrong or share some great tips for others to consider and discuss. ✨
How to Use This Document
- 👀 Jump in wherever you like. Each section compares a React/Next.js pattern to its Vue/Nuxt equivalent. If you already know slots, skip ahead to provide/inject or routing.
- 🧪 Treat the code-labs as exercises. They’re short, focused challenges designed to reinforce the concept. Copy, paste, break things, and make them your own.
- 🤷🏻 Don’t expect it to be finished. This is a living document. I’ll update, refine, and expand as I encounter new patterns and idioms in Vue/Nuxt.
- 🧑🏻🎓 Use it as a bridge. The goal is not to re-learn frontend from scratch, but to lean on your React knowledge while translating concepts into Vue’s mental model.
- 🐿️ Stay curious. When in doubt, follow the links to the official React, Vue, and Nuxt docs — this doc is a companion, not a replacement.
👷🏻 Setup (fast)
- Scaffold Vue: Vite + SFC + TS ready.
npm create vue@latest
-
Scaffold Nuxt: →
pnpm i && pnpm dev
npx nuxi@latest init my-app
Data: useAsyncData
, useFetch
, pages/ routing
🧠 Mental model (reframe)
Reactivity is granular:
derive withcomputed
, side-effects withwatch
/watchEffect
/ (API).SFCs with
<script setup>
are idiomatic; macros provide TS-friendly DX:
<script setup>
, SFC spec.Slots are Vue's "children" (scoped slot props):
Slots / 3.x unification.Context → provide/inject (typed with
InjectionKey
):
Provide/Inject, API +InjectionKey<T>
.Portals/Keep state:
<Teleport>
,<KeepAlive>
.Component v-model (two-way):
v-model
&defineModel
(3.4), Announcing 3.4.Suspense exists but is experimental; in SSR prefer Nuxt's data composables:
<Suspense>
, Nuxt data fetching.
Pattern translations + mini code-labs
Each section: React → Vue (+ Nuxt twist). Do the steps; verify visually.
1) Children/compound components → Slots & named slots
React
<Card>
<Card.Header />
<Card.Body />
<Card.Footer />
</Card>
Vue (SFC)
<!-- components/Card.vue -->
<template>
<div class="card">
<header><slot name="header" /></header>
<section><slot /></section>
<footer><slot name="footer" /></footer>
</div>
</template>
<!-- usage -->
<Card>
<template #header>Title</template>
Content
<template #footer>Actions</template>
</Card>
Code-lab
- Build
Card.vue
with named slots. - Add slot props: pass
sectionId
from parent ->#default="{ sectionId }"
. - Stretch: create
<Card.Header/>
etc. as wrappers that render named slots.
Refs: Slots
🤔 But wait!
I'm not sure about this pattern. React reads much closer to native html markup <Card.Header />
, while it clearly belongs to the parent <Card />
it remains it's own unit of functionality.
What if we went further to wrap the inner template
components in named slots like <Card.Header />
etc so it can read in the same way in Vue. Maybe it wouldn't have any actual functional meaning but could replicate the great readability of React.
But hang on, we need to think less "react" and more "vue" paradigms. Perhaps this isn't highlighting a drawback of Vue but rather the limitations of React?
By writing code as <template #header>
we semantically mark it not only a native template but also as a header
slot, not just for a Card
but anything! The code is therefore not only reusable but transferrable too 🤯
Does this mean we have more control and say over where we want to separate our concerns? Do we in fact get more semantic meaning this way...?
2) Context → provide/inject
React
const Theme = createContext<'light'|'dark'>('light')
<Theme.Provider value="dark"><App/></Theme.Provider>
Vue
// App.vue
<script setup lang="ts">
import { provide } from 'vue'
const ThemeKey = Symbol() as InjectionKey<'light'|'dark'>
provide(ThemeKey, 'dark')
</script>
// Leaf.vue
<script setup lang="ts">
import { inject } from 'vue'
const theme = inject(ThemeKey, 'light')
</script>
Code-lab
- Provide a reactive
ref('light')
and a toggle function. - Inject in deep child and verify updates.
- TS: use
InjectionKey<T>
for type-safe provide/inject.
Refs: Provide/Inject, API w/ InjectionKey
3) Hooks → Composables
React
function useMouse(){ /* setState/effect */ }
Vue
// composables/useMouse.ts
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse(){
const x = ref(0), y = ref(0)
const move = (e: MouseEvent)=>{ x.value=e.pageX; y.value=e.pageY }
onMounted(()=> window.addEventListener('mousemove', move))
onUnmounted(()=> window.removeEventListener('mousemove', move))
return { x, y }
}
Code-lab
- Create
composables/useMouse.ts
and display{ x }/{ y }
in a component. - Stretch: add throttling (requestAnimationFrame) and SSR-guard window access.
Refs: Community: VueUse
4) useMemo
/ useCallback
→ computed
React
const total = useMemo(()=> items.reduce(...), [items])
Vue
import { computed } from 'vue'
const total = computed(()=> items.value.reduce(...))
Code-lab
- Convert a derived selector to
computed
. - Add a heavy calc; confirm it recomputes only when deps change (log).
Refs: Computed
5) useEffect
side-effects → onMounted / watch / watchEffect
React
useEffect(()=>{ const id=setInterval(tick,1000); return ()=>clearInterval(id) }, [])
Vue
import { onMounted, onBeforeUnmount } from 'vue'
onMounted(()=>{ const id=setInterval(tick,1000); onBeforeUnmount(()=>clearInterval(id)) })
Code-lab
- Replace a
useEffect
data sync withwatch(source, cb)
. - Use
watchEffect
for auto-tracked dependencies; compare tocomputed
.
Refs: Watchers, Reactivity API
6) Controlled inputs → v-model
/ defineModel
React
<input value={name} onChange={e=>setName(e.target.value)} />
Vue
<input v-model="name" />
Child two-way (3.4)
<!-- Child.vue -->
<script setup lang="ts">
const model = defineModel<string>()
</script>
<template><input v-model="model" /></template>
Code-lab
- Build a reusable
<TextField v-model="name" />
with validation. - Add multiple models:
defineModel('start')
,defineModel('end')
. - Try
.trim
/.number
modifiers.
Refs: Component v-model + defineModel
, Vue 3.4
7) Portals → Teleport
React
return createPortal(<Modal/>, document.body)
Vue
<Teleport to="body">
<Modal />
</Teleport>
Code-lab
- Render a modal/tooltip via
<Teleport to=\"#modals\"/>
(dedicated node). - SSR note: verify target exists client-side.
Refs: <Teleport>
8) Error boundaries → onErrorCaptured
/ Nuxt error pages
React: componentDidCatch
/error boundaries.
Vue:
import { onErrorCaptured } from 'vue'
onErrorCaptured((err, instance, info)=>{ /* log */ return false })
Nuxt
- Global: Error handling,
error.vue
page.
Code-lab
- Throw in a child; handle with
onErrorCaptured
and display fallback UI. - Nuxt: create
error.vue
and simulate an API failure.
Refs: Vue error handling API, Nuxt errors
9) Suspense & async UI
- React: Suspense/RSC orchestrate async and streaming.
-
Vue:
<Suspense>
experimental; prefer Nuxt data APIs for SSR.
Nuxt data
<script setup lang="ts">
const { data, pending, error } = await useAsyncData('posts', () => $fetch('/api/posts'))
</script>
<template>
<PostList v-if="data" :items="data" />
<Skeleton v-else-if="pending" />
<ErrorBox v-else :error="error" />
</template>
Code-lab
- Implement the above; confirm no double fetch on hydrate.
- Compare
useFetch
vsuseAsyncData
for GET/POST. (Docs: Data fetching)
10) Routing (React Router/Next) → Vue Router/Nuxt
- Vue Router: nested routes, guards, programmatic nav. Docs: Guide, Guards, Composition API
Vue Router example
// router.ts
import { createRouter, createWebHistory } from 'vue-router'
export const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: () => import('./pages/Home.vue') },
{ path: '/admin', component: () => import('./pages/Admin.vue'), meta:{auth:true} }
]
})
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).use(router).mount('#app')
Guard
router.beforeEach((to, _from, next)=>{
if (to.meta.auth && !isAuthed()) return next('/login')
next()
})
Nuxt
Code-lab
- Port an authenticated section with a global guard.
- Nuxt: create
pages/admin.vue
; add navigation middleware; test redirects.
Cheat Sheet
React → Vue Cheat-Sheet
Bonus: ecosystem mappings
- State: Redux/Zustand → Pinia (docs, Vuex note).
-
Transitions:
<Transition>
/<TransitionGroup>
built-in. -
Preserve component state:
<KeepAlive>
(docs). -
Client-only/islands: Nuxt
<ClientOnly>
+<NuxtIsland>
.
Vue-native best practices (for React folks)
- Prefer
<script setup>
+ macros (defineProps/Emits/Model
,withDefaults
) for clean TS. - Use
computed
for derivations,watch
/watchEffect
for side-effects. -
Don't destructure props/reactive objects unless via
toRefs/toRef
(keep reactivity). - Reach for composables for shared logic; study patterns in VueUse.
- Be SSR-aware (no direct
window
/document
on server); if needed, gate withprocess.client
in Nuxt. - For modals/menus, prefer Teleport to dedicated container elements.
- Performance: start with defaults-fine-grained tracking does heavy lifting. Only micro-opt once measured.
Primary docs: Computed, Watchers, <script setup>
, Provide/Inject, Teleport
Nuxt for Next engineers (quick map)
-
Data:
useAsyncData
/useFetch
(SSR-first; payload de-dupes client refetch). Docs: useAsyncData, useFetch -
Server routes:
server/api/*.ts
(Nitro). -
Routing:
pages/
filesystem; nested layouts & middleware. -
Islands:
<NuxtIsland>
; client-only:<ClientOnly>
-
Errors: Error handling,
error.vue
4-week self-study plan
- W1: Rebuild a small React app in Vue SFCs; slots + composables + Pinia.
- W2: Router 4 (guards, nested layouts); transitions; Teleport + accessibility.
- W3: Migrate to Nuxt: file routing, data fetching, API routes, error pages.
-
W4: Production polish-payload caching,
<KeepAlive>
, perf checks, CI build.
Appendix: official docs index
- Vue core: Computed · Watchers ·
<script setup>
· SFC · Provide/Inject · Teleport · KeepAlive · Suspense (exp.) - Router: Guide · Guards · Composition API
- Pinia: Docs
- Nuxt: Data fetching · pages/ · Errors · ClientOnly · NuxtIsland
This content originally appeared on DEV Community and was authored by Graeme George

Graeme George | Sciencx (2025-08-28T17:17:04+00:00) Next Stop, Nuxt: A React Engineer’s Journey into Vue. Retrieved from https://www.scien.cx/2025/08/28/next-stop-nuxt-a-react-engineers-journey-into-vue/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.