تطوير الويب

الهجرة من Next.js App Router نحو TanStack Start: خطة ومزالق 2026

4 min de lecture

السلسلة: هذا الدرس جزء من سلسلة TanStack. اقرأ المقال الرئيسي.

هجرة تطبيق Next.js App Router إنتاجي نحو TanStack Start ليس قراراً يُتَّخذ باستخفاف. يجب وزن الفوائد (قراءة، type-safety كاملة، نقل multi-runtime) ضد التكاليف (إعادة كتابة أنماط server-first، خسارة مؤقتة لنظام بيئي، منحنى تعلّم). يقترح هذا الدرس خطة هجرة تدريجية، جدول مكافئات API مفصَّل، استراتيجيات لنقل Server Components، وإدارة إعادات SEO.

المتطلبات

  • تطبيق Next.js 14 أو 15 إنتاجي مع App Router
  • مطوّر متقدم بدوام كامل على الأقل
  • معرفة عملية بـ Server Components وServer Actions وcache fetch
  • بيئة staging معزولة
  • وصول لسجلات الإنتاج

الخطوة 1 — تقييم ملاءمة الهجرة

ثلاثة أسئلة ملموسة:

  • هل الفريق يصارع بانتظام cache fetch أو توجيه 'use server'؟ TanStack Start يحلّ هذا الألم بصراحة تجعل حالة التطبيق مقروءة.
  • هل المشروع يحتاج مغادرة Vercel نحو Cloudflare، Bun، AWS Lambda أو VPS؟ runtime Nitro لـ Start ينشر في كل مكان.
  • هل المشروع يحتاج type-safety صارمة على routes وsearch params؟ TanStack Router يفعل هذا أصلياً.

إن كانت الإجابة نعم على اثنين من ثلاثة، الهجرة تستحق. وإلا، ابقَ على Next.js.

الخطوة 2 — رسم خريطة التطبيق الموجود

سبع فئات للجرد:

  • كل routes app/**/page.tsx
  • كل layout.tsx ومداها
  • كل Server Actions (ملفات أو inline مع 'use server')
  • كل routes API app/api/**/route.ts
  • كل middlewares (middleware.ts في الجذر)
  • كل المكوّنات التي تستخدم hooks خاصة Next (useRouter، usePathname، useSearchParams)
  • كل الإعدادات الخاصة (next.config.js، redirects، rewrites، image optimization)

الخطوة 3 — اختيار استراتيجية تبديل

الاستراتيجية متى تستخدمها المدة النمطية
Strangler مع reverse proxy تطبيق حرج، SEO حساس، فريق متوسط 4-8 أشهر
تعايش على نطاق فرعي ميزة جديدة معزولة، تطبيق legacy مجمَّد 2-4 أشهر
إعادة كتابة حسب module تطبيق صغير، فريق greenfield 2-5 أشهر

استراتيجية strangler الأكثر أماناً. reverse proxy (Nginx، Cloudflare Workers، Caddy) يوجّه بعض URLs نحو التطبيق الجديد والباقي نحو القديم. التبديل يصير تغيير config في proxy.

الخطوة 4 — Bootstrap مشروع TanStack Start

npm create tanstack@latest my-app-v2
cd my-app-v2
npm install
npm run dev

ثبّت التبعيات التي يستخدمها التطبيق legacy أيضاً: Drizzle أو Prisma، Zod، client القاعدة، مكتبات UI.

الخطوة 5 — تحويل routing app/ إلى src/routes/

Next.js App Router TanStack Router
app/page.tsx src/routes/index.tsx
app/about/page.tsx src/routes/about.tsx
app/blog/[slug]/page.tsx src/routes/blog/$slug.tsx
app/[...slug]/page.tsx src/routes/$.tsx
app/layout.tsx (root) src/routes/__root.tsx
app/(marketing)/page.tsx src/routes/(marketing)/index.tsx
app/dashboard/layout.tsx src/routes/dashboard/route.tsx
app/_protected/layout.tsx (pathless) src/routes/_protected.tsx

فخّ كلاسيكي: Next يستخدم [slug] بأقواس مربعة، TanStack Router يستخدم $slug بـ dollar. بحث-استبدال سريع يؤتمت 90% من العمل.

الخطوة 6 — نقل Server Components

TanStack Start لا يستخدم React Server Components. الأنماط الشائعة لها مكافئات مباشرة.

// قبل (Next.js Server Component)
async function PostList() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json())
  return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
}

// بعد (TanStack Start)
import { queryOptions, useSuspenseQuery } from '@tanstack/react-query'
import { createFileRoute } from '@tanstack/react-router'

const postsQuery = queryOptions({
  queryKey: ['posts'],
  queryFn: () => fetch('https://api.example.com/posts').then(r => r.json()),
})

export const Route = createFileRoute('/posts/')({
  loader: ({ context }) => context.queryClient.prefetchQuery(postsQuery),
  component: PostList,
})

function PostList() {
  const { data } = useSuspenseQuery(postsQuery)
  return <ul>{data.map((p: any) => <li key={p.id}>{p.title}</li>)}</ul>
}

الخطوة 7 — تحويل Server Actions إلى server functions

// قبل (Next.js Server Action)
'use server'
import { z } from 'zod'

export async function createPost(data: { title: string }) {
  const parsed = z.object({ title: z.string().min(3) }).parse(data)
  return await db.insert(posts).values(parsed).returning()
}

// بعد (TanStack Start server function)
import { createServerFn } from '@tanstack/react-start'
import { z } from 'zod'

export const createPost = createServerFn({ method: 'POST' })
  .inputValidator(z.object({ title: z.string().min(3) }))
  .handler(async ({ data }) => {
    return await db.insert(posts).values(data).returning()
  })

الاستدعاء جانب المكوّن يصير await createPost({ data: { title: 'test' } }). للإلغاء، نستبدل revalidatePath بـ queryClient.invalidateQueries.

الخطوة 8 — هجرة routes API

// قبل (Next.js app/api/posts/route.ts)
export async function GET(request: Request) {
  const posts = await db.select().from(postsTable)
  return Response.json(posts)
}

// بعد (TanStack Start src/routes/api/posts.ts)
import { createFileRoute } from '@tanstack/react-router'
import { db } from '~/db/client'
import { posts as postsTable } from '~/db/schema'

export const Route = createFileRoute('/api/posts')({
  server: {
    handlers: {
      GET: async () => {
        const posts = await db.select().from(postsTable)
        return Response.json(posts)
      },
    },
  },
})

الخطوة 9 — استبدال hooks الخاصة بـ Next

Next.js TanStack Router
useRouter() useRouter() (API مختلفة)
router.push('/x') navigate({ to: '/x' })
usePathname() useLocation().pathname
useSearchParams() useSearch() (typé)
useParams() useParams({ from: '/route/$id' })
redirect('/login') throw redirect({ to: '/login' })
notFound() throw notFound()

الخطوة 10 — إدارة إعادات SEO بلا خسارة

// مثال Worker frontal لإعادات
const REDIRECTS: Record<string, string> = {
  '/products': '/produits',
  '/blog/old-slug': '/blog/new-slug',
}

export default {
  async fetch(request: Request) {
    const url = new URL(request.url)
    if (REDIRECTS[url.pathname]) {
      return Response.redirect(url.origin + REDIRECTS[url.pathname], 301)
    }
    return fetch(request)
  },
}

راقب Google Search Console أسبوعين إلى أربعة بعد كل موجة تبديل. أرسل URLs الجديدة عبر IndexNow.

فروق للحفظ

الجانب Next.js App Router TanStack Start
Metadata صفحة Export metadata head() في route
Loading UI loading.tsx pendingComponent
Error UI error.tsx errorComponent
Image optimization next/image حل خارجي (unpic، Cloudflare Images)
Font optimization next/font fontsource أو self-hosted
Middleware Edge middleware.ts Worker Cloudflare في الأمام
Streaming RSC streaming أصلي defer في loader
ISR revalidate على fetch staleTime Query + cache CDN

أخطاء شائعة

الخطأ السبب الحل
Hydration mismatch على تواريخ مكوّنات تستخدم new Date() على حدّ SSR/client انقل في useEffect أو suppressHydrationWarning
Cache Query يُعاد تعيينه عند كل تنقل QueryClient مُنشأ خارج router دائماً عبر createRouter
Server Action مهاجَر لكن غير مستخدَم المكوّن المُهاجَر لا يزال يستخدم useFormState استبدل بـ useMutation من Query
404 على /api/auth/* بعد هجرة auth route catch-all مفقود أنشئ src/routes/api/auth/$.ts
Metadata مفقودة بعد النقل Export metadata غير مستبدَل بـ head() استخدم head() في createFileRoute
Bundle أكبر من المتوقَّع imports ديناميكية مفقودة عند التحويل فعّل autoCodeSplitting: true

أسئلة شائعة

كم تستغرق هجرة متوسطة؟ لتطبيق 30 route مع 10 server actions وmodule auth، احسب 6-12 أسبوع بدوام كامل لمطوّر متقدم. فريق اثنين يقسم تقريباً النصف.

هل يمكن الهجرة تدريجياً مع تطبيقين متوازيين؟ نعم، حتى موصى به. نمط strangler خلف reverse proxy يسمح باختبار كل route على نسب صغيرة من حركة المرور.

كيف ننقل next/image؟ لصور من نطاق مخصّص، استخدم مكوّن Image من مكتبة unpic الذي يدعم Cloudinary، ImageKit، Cloudflare Images.

وهل سيبقى SEO محفوظاً؟ نعم، شرط وضع إعادات 301 لأي URL يتغيّر ومراقبة Search Console.

هل من الأفضل انتظار 1.0 المستقرّ؟ ليس بالضرورة. RC مُعلن كـ build الذي سيخرج في 1.0. الهجرة الآن تعطي تقدماً 6-12 شهراً.

مقالات ذات صلة

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité