DEV Community

Cover image for Vue.js vs Next.js: Modal Routing — A Side-by-Side Breakdown
Heba Allah Hashim
Heba Allah Hashim

Posted on

Vue.js vs Next.js: Modal Routing — A Side-by-Side Breakdown

Let's look at the breakdown of both frameworks side-by-side to understand the massive shift in mindset.


Project 1: The Vue.js Approach (Code-Driven Logic)

Step 1: Initialize the Project

npm create vite@latest vue-modal-app -- --template vue
cd vue-modal-app
npm install vue-router@4
Enter fullscreen mode Exit fullscreen mode

Step 2: Configure the Routing Paths (src/router.js)

import { createRouter, createWebHistory } from 'vue-router'
import GalleryView from './views/GalleryView.vue'

export const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      component: GalleryView,
      children: [
        {
          path: 'photo/:id',
          component: () => import('./components/PhotoModal.vue'),
          props: true
        }
      ]
    }
  ]
})
Enter fullscreen mode Exit fullscreen mode

Step 3: Link the Router to the Application

Update src/main.js:

import { createApp } from 'vue'
import App from './App.vue'
import { router } from './router'

createApp(App).use(router).mount('#app')
Enter fullscreen mode Exit fullscreen mode

Replace src/App.vue:

<template>
  <router-view />
</template>
Enter fullscreen mode Exit fullscreen mode

Step 4: Create the Gallery List Page (src/views/GalleryView.vue)

<template>
  <div style="padding: 20px; font-family: sans-serif;">
    <h1>Vue Photo Gallery</h1>

    <div style="display: flex; gap: 20px;">
      <div 
        v-for="photoId in ['sunset', 'ocean', 'mountains']" 
        :key="photoId"
        @click="openPhoto(photoId)"
        style="width: 150px; height: 100px; background: #3b82f6; color: white; display: flex; align-items: center; justify-content: center; cursor: pointer; border-radius: 8px;"
      >
        View {{ photoId }}
      </div>
    </div>

    <!-- Renders our Child Popup component over the grid -->
    <router-view v-if="showModal" @close="closeModal" />
  </div>
</template>

<script setup>
import { ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'

const route = useRoute()
const router = useRouter()
const showModal = ref(false)

// Crucial: The Watcher forces the modal open if an ID exists on hard refresh
watch(
  () => route.params.id,
  (newId) => {
    showModal.value = !!newId
  },
  { immediate: true }
)

function openPhoto(id) {
  router.push(`/photo/${id}`)
}

function closeModal() {
  router.push('/')
}
</script>
Enter fullscreen mode Exit fullscreen mode

Step 5: Create the Popup Modal Component (src/components/PhotoModal.vue)

<template>
  <div style="position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 9999;">
    <div style="background: white; padding: 30px; border-radius: 12px; text-align: center; width: 300px;">
      <h2>Vue Modal Layout</h2>
      <p style="font-size: 24px; font-weight: bold; text-transform: uppercase; color: #3b82f6;">
        🌌 {{ id }}
      </p>
      <button @click="$emit('close')" style="margin-top: 20px; padding: 8px 16px; cursor: pointer;">Close Overlay</button>
    </div>
  </div>
</template>

<script setup>
defineProps({ id: String })
defineEmits(['close'])
</script>
Enter fullscreen mode Exit fullscreen mode

Run via npm run dev to view.


Project 2: The Next.js Approach (Folder-Driven Architecture)

Step 1: Initialize the Project

# Configuration flags: TypeScript (Yes), ESLint (No), src/ (No), App Router (Yes)
npx create-next-app@latest next-modal-app
cd next-modal-app
Enter fullscreen mode Exit fullscreen mode

Step 2: Create the File Folders

Manually create the exact folders matching the Hierarchy Map above directly inside your editor's file sidebar to prevent system escape errors.

Step 3: Set up the Main Gallery Page (app/gallery/page.tsx)

import Link from 'next/link';

export default function GalleryPage() {
  return (
    <div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
      <h1>Next.js Photo Gallery</h1>

      <div style={{ display: 'flex', gap: '20px' }}>
        {['sunset', 'ocean', 'mountains'].map((photoId) => (
          <Link 
            key={photoId} 
            href={`/gallery/photo/${photoId}`}
            style={{ 
              width: '150px', 
              height: '100px', 
              background: '#ef4444', 
              color: 'white', 
              display: 'flex', 
              alignItems: 'center', 
              justifyContent: 'center',
              textDecoration: 'none', 
              borderRadius: '8px' 
            }}
          >
            View {photoId}
          </Link>
        ))}
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Configure the Layout Slots (app/gallery/layout.tsx)

export default function GalleryLayout({
  children,
  modal
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <div>
      {children}
      {modal}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Create the Slot Fallback (app/gallery/@modal/default.tsx)

export default function DefaultModal() {
  // Fixes the unmounted slot mismatch crash by returning a baseline null
  return null;
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Create the Intercepted Popup View (app/gallery/@modal/(.)photo/[id]/page.tsx)

'use client';
import { useRouter, useParams } from 'next/navigation';

export default function InterceptedPhotoPopup() {
  const router = useRouter();
  const params = useParams(); 

  return (
    <div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 9999 }}>
      <div style={{ background: 'white', padding: '30px', borderRadius: '12px', textAlign: 'center', width: '300px' }}>
        <h2>Next.js Intercepted Popup</h2>
        <p style={{ fontSize: '24px', fontWeight: 'bold', textTransform: 'uppercase', color: '#ef4444' }}>
          📸 {params.id}
        </p>
        <button onClick={() => router.back()} style={{ marginTop: '20px', padding: '8px 16px', cursor: 'pointer' }}>
          Close Overlay
        </button>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Step 7: Create the Standalone Fallback Page (app/gallery/photo/[id]/page.tsx)

import Link from 'next/link';

interface PageProps {
  params: Promise<{ id: string }>;
}

export default async function StandalonePhotoPage({ params }: PageProps) {
  // Modern Next.js rule: params must be unwrapped asynchronously
  const { id } = await params;

  return (
    <div style={{ padding: '40px', fontFamily: 'sans-serif', textAlign: 'center' }}>
      <h1>Standalone Full-Screen View</h1>
      <div style={{ margin: '40px auto', width: '400px', height: '250px', background: '#ef4444', color: 'white', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '32px', borderRadius: '12px' }}>
        🌌 {id.toUpperCase()}
      </div>
      <Link href="/gallery" style={{ color: '#ef4444' }}>← Back to Gallery List</Link>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Run via npm run dev and navigate to http://localhost:3000/gallery.

Top comments (0)