DEV Community

Cover image for Next Stop, Nuxt: A React Engineer’s Journey into Vue
Graeme George
Graeme George

Posted on

Next Stop, Nuxt: A React Engineer’s Journey into Vue

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
Enter fullscreen mode Exit fullscreen mode
  • Scaffold Nuxt: → pnpm i && pnpm dev
npx nuxi@latest init my-app
Enter fullscreen mode Exit fullscreen mode

Data: useAsyncData, useFetch, pages/ routing


🧠 Mental model (reframe)


Pattern translations + mini code-labs

Each section: ReactVue (+ Nuxt twist). Do the steps; verify visually.

1) Children/compound components → Slots & named slots

React

<Card>
  <Card.Header />
  <Card.Body />
  <Card.Footer />
</Card>
Enter fullscreen mode Exit fullscreen mode

Vue (SFC)

<!-- components/Card.vue -->
<template>
  <div class="card">
    <header><slot name="header" /></header>
    <section><slot /></section>
    <footer><slot name="footer" /></footer>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode
<!-- usage -->
<Card>
  <template #header>Title</template>
  Content
  <template #footer>Actions</template>
</Card>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 */ }
Enter fullscreen mode Exit fullscreen mode

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 }
}
Enter fullscreen mode Exit fullscreen mode

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 / useCallbackcomputed

React

const total = useMemo(()=> items.reduce(...), [items])
Enter fullscreen mode Exit fullscreen mode

Vue

import { computed } from 'vue'
const total = computed(()=> items.value.reduce(...))
Enter fullscreen mode Exit fullscreen mode

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) }, [])
Enter fullscreen mode Exit fullscreen mode

Vue

import { onMounted, onBeforeUnmount } from 'vue'
onMounted(()=>{ const id=setInterval(tick,1000); onBeforeUnmount(()=>clearInterval(id)) })
Enter fullscreen mode Exit fullscreen mode

Code-lab

  • Replace a useEffect data sync with watch(source, cb).
  • Use watchEffect for auto-tracked dependencies; compare to computed.

Refs: Watchers, Reactivity API


6) Controlled inputs → v-model / defineModel

React

<input value={name} onChange={e=>setName(e.target.value)} />
Enter fullscreen mode Exit fullscreen mode

Vue

<input v-model="name" />
Enter fullscreen mode Exit fullscreen mode

Child two-way (3.4)

<!-- Child.vue -->
<script setup lang="ts">
const model = defineModel<string>()
</script>
<template><input v-model="model" /></template>
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Vue

<Teleport to="body">
  <Modal />
</Teleport>
Enter fullscreen mode Exit fullscreen mode

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 })
Enter fullscreen mode Exit fullscreen mode

Nuxt

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>
Enter fullscreen mode Exit fullscreen mode

Code-lab

  • Implement the above; confirm no double fetch on hydrate.
  • Compare useFetch vs useAsyncData for GET/POST. (Docs: Data fetching)

10) Routing (React Router/Next) → Vue Router/Nuxt

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')
Enter fullscreen mode Exit fullscreen mode

Guard

router.beforeEach((to, _from, next)=>{
  if (to.meta.auth && !isAuthed()) return next('/login')
  next()
})
Enter fullscreen mode Exit fullscreen mode

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 with process.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)


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

Top comments (0)