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-shee…


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)

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>

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

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 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)} />

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

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 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')

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


This content originally appeared on DEV Community and was authored by Graeme George


Print Share Comment Cite Upload Translate Updates
APA

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

MLA
" » Next Stop, Nuxt: A React Engineer’s Journey into Vue." Graeme George | Sciencx - Thursday August 28, 2025, https://www.scien.cx/2025/08/28/next-stop-nuxt-a-react-engineers-journey-into-vue-2/
HARVARD
Graeme George | Sciencx Thursday August 28, 2025 » Next Stop, Nuxt: A React Engineer’s Journey into Vue., viewed ,<https://www.scien.cx/2025/08/28/next-stop-nuxt-a-react-engineers-journey-into-vue-2/>
VANCOUVER
Graeme George | Sciencx - » Next Stop, Nuxt: A React Engineer’s Journey into Vue. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/08/28/next-stop-nuxt-a-react-engineers-journey-into-vue-2/
CHICAGO
" » Next Stop, Nuxt: A React Engineer’s Journey into Vue." Graeme George | Sciencx - Accessed . https://www.scien.cx/2025/08/28/next-stop-nuxt-a-react-engineers-journey-into-vue-2/
IEEE
" » Next Stop, Nuxt: A React Engineer’s Journey into Vue." Graeme George | Sciencx [Online]. Available: https://www.scien.cx/2025/08/28/next-stop-nuxt-a-react-engineers-journey-into-vue-2/. [Accessed: ]
rf:citation
» Next Stop, Nuxt: A React Engineer’s Journey into Vue | Graeme George | Sciencx | https://www.scien.cx/2025/08/28/next-stop-nuxt-a-react-engineers-journey-into-vue-2/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.