الواجهة البرمجية للكائن router في Next.js

من موسوعة حسوب

ننصحك قبل قراءة المقال بالاطلاع على التوجّه في Next.js أولًا

استخدام الخطاف useRouter

إن أردت الوصول إلى الكائن router ضمن أي مكوّن دالة في تطبيقك، استخدم الخطاف useRouter. إليك مثالًا:

import { useRouter } from 'next/router'

function ActiveLink({ children, href }) {
  const router = useRouter()
  const style = {
    marginRight: 10,
    color: router.asPath === href ? 'red' : 'black',
  }

  const handleClick = (e) => {
    e.preventDefault()
    router.push(href)
  }

  return (
    <a href={href} onClick={handleClick} style={style}>
      {children}
    </a>
  )
}

export default ActiveLink

useRouter هو خطاف React، ويعني ذلك عدم إمكانية استخدامه مع الأصناف. لهذا يمكنك استخدام withRouter أو تغليف الصنف ضمن مكوّن دالة.

الكائن router في Next.js

إليك تعريف الكائن router الذي يعيده الخطافان useRouter و withRouter:

  • pathname: من النوع String، ويعيد الوجهة الحالية. ويعني ذلك مسار الصفحة ضمن المجلد pages/، لا تتضمن الوجهة قيمة المسار الجذري basePath أو الإعداد المحلي locale.
  • query: نص الاستعلام وقد حُلِّل إلى كائن، ويتضمن معاملات الوجهة الديناميكية. سيكون هذا الكائن فارغًا خلال التصيير المسبق إن لم تستخدم التصيير من جانب الخادم.
  • asPath: من النوع String، وهو المسار الذي يضم الاستعلام ويُعرض في الماصفح دون المسار الجذري basePath والإعداد المحلي locale.
  • isFallback: من النوع boolean، ويدل إن كانت الصفحة الحالة في وضع التراجع fallback.
  • basePath: المسار الجذري الفعّال (إن مُكِّن).
  • locale: من النوع String، وهو الإعداد المحلي المفعّل (إم مُكِّن).
  • defaultLocale: الإعداد المحلي الافترااضي (إن مُكِّن).
  • domainLocales: مصفوفة من الشكل <Array<{domain, defaultLocale, locales} وتضم أية نطاقات محلية مُعدّة.
  • isReady: من النوع boolean، ويُظهر إن كانت حقول الكائن router قد حُدِّثت في طرف العميل وجاهزة للاستخدام. ينبغي استخدامها فقط ضمن توابع الخطاف useEffect وليس للتصيير الشرطي على الخادم. اطلع على التوثيق المتعلق بهذه الحالة في صفحة التحسين التلقائي الساكن للصفحات.
  • isPreview: من النوع boolean، ويظهر إن كان التطبيق في وضع الاستعراض preview mode أم لا.

قد يؤدي استخدام الحقل asPath إلى خطأ في التطبيق بين العميل والخادم إن صُيِّرت الصفحة من قبل الخادم أو باستخدام التحسين التلقائي الساكن. تفادى استخدام الحقل asPath حتى تكون قيمة isReady هي "true محقق".

يضم الكائن router مجموعة من التوابع سنستعرضها تاليًا

التابع router.push

ويعالج التنقل بين الصفحات من جانب العميل، وتظهر فائدته في الحالات التي لا يكفي فيها استخدام المكوّن next/link:

router.push(url, as, options)
  • url: وقد يكون من أحد النوعين UrlObject | String، ويمثّل عنوان للوجهة (راجع توثيق Node.js المتعلق بالكائن urlobject).
  • as: منسّق اختياري للمسار الذي سيُعرض في شريط عناوين المتصفح. استُخدم هذا المعامل سابقًا للتوجه الديناميكي ما قبل الإصدار 9.5.3.
  • options: كائن اختياري له أيضًا مجموعة من الخيارات:
    • scroll: من النوع المنطقي boolean، ويتحكم بالتمرير إلى أعلى الصفحة بعد الانتقال، وقيمته الافتراضية true.
    • shallow: يُحدّث مسار الصفحة الحالية دون تنفيذ الدوال getStaticProps أو getServerSideProps أو getInitialProps، وقيمته الافتراضية false.
    • locale: نص اختياري يشير إلى الإعداد المحلي للصفحة.

لا حاجة لا ستخدام router.push لعنواين URL الخارجية، ويُفضّل في هذه الحالة استخدام window.location.

طريقة استخدامه

الإنتقال إلى pages/about.js وهي وجهة محددة سلفًا:

import { useRouter } from 'next/router'

export default function Page() {
  const router = useRouter()

  return (
    <button type="button" onClick={() => router.push('/about')}>
      Click me
    </button>
  )
}

الانتقال إلى pages/post/[pid].js وهي وجهات ديناميكية:

import { useRouter } from 'next/router'

export default function Page() {
  const router = useRouter()

  return (
    <button type="button" onClick={() => router.push('/post/abc')}>
      Click me
    </button>
  )
}

إعادة توجيه المستخدم إلى الصفحة pages/login.js وهذا مفيد عندما تريد الانتقال إلى صفحة بعد الاستيثاق:

import { useEffect } from 'react'
import { useRouter } from 'next/router'

// Here you would fetch and return the user
const useUser = () => ({ user: null, loading: false })

export default function Page() {
  const { user, loading } = useUser()
  const router = useRouter()

  useEffect(() => {
    if (!(user || loading)) {
      router.push('/login')
    }
  }, [user, loading])

  return <p>Redirecting...</p>
}

إعادة ضبط الحالة بعد الانتقال

عند العودة إلى نفس الصفحة في Next.js ،لن يعاد ضبط حالة الصفحة افتراضيًا لأن مكوّن React لن يُزال من شجرة DOM حتى يتغيّر المكوّن الأب.

// pages/[slug].js
import Link from 'next/link'
import { useState } from 'react'
import { useRouter } from 'next/router'

export default function Page(props) {
  const router = useRouter()
  const [count, setCount] = useState(0)
  return (
    <div>
      <h1>Page: {router.query.slug}</h1>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increase count</button>
      <Link href="/one">
        <a>one</a>
      </Link> <Link href="/two">
        <a>two</a>
      </Link>
    </div>
  )
}

لن يُصفَّر العداد عند الانتقال بين الصفحتين one/ و two/ في المثال السابق. وسيبقى خطاف الحالة useState دون تغيير ما بين عمليات التصيير لأن المكوّن الأب Page يبقى نفسه.

إن لم ترغب بهذا السلوك، لديك خياران:

  1. تأكد من تحديث كل حالة يدويًا باستخدام الخطاف useEffect. قد يبدو الأمر في المثال السابق كما يلي:
useEffect(() => {
  setCount(0)
}, [router.query.slug])

2. استخدم الخاصية key في React لإخبارها بإعادة تثبيت المكوّن. ولتطبق الأمر على جميع الصفحات، بإمكانك استخدام تطبيق App مخصص:

// pages/_app.js
import { useRouter } from 'next/router'

export default function MyApp({ Component, pageProps }) {
  const router = useRouter()
  return <Component key={router.asPath} {...pageProps} />
}

مع كائن URL

بإمكانك استخدام كائن URL بنفس الطريقة التي تستخدمه فيها مع next/link، ويعمل مع كلا المعاملين url و as:

import { useRouter } from 'next/router'

export default function ReadMore({ post }) {
  const router = useRouter()

  return (
    <button
      type="button"
      onClick={() => {
        router.push({
          pathname: '/post/[pid]',
          query: { pid: post.id },
        })
      }}
    >
      Click here to read more
    </button>
  )
}

التابع router.replace

يشابه الخاصة replace في next/link، إذ يمنع router.replace إضافة عنوان url جديد إلى المٌكدِّس history.

router.replace(url, as, options)

تتشابه الواجهة البرمجية للتابع router.replace مع الواجهة البرمجية للتابع router.push.

طريقة استخدامه

الق نظرة على المثال التالي:

import { useRouter } from 'next/router'

export default function Page() {
  const router = useRouter()

  return (
    <button type="button" onClick={() => router.replace('/home')}>
      Click me
    </button>
  )
}

التابع router.prefetch

يحضر الصصفحات مسبقًا لتأمين تنقل أسرع بين الصفحات في طرف العميل. تكمن فائدة هذا التابع عند استعماله مع next/link فقط، لأن المكوّن next/link يهتم بإحضار الصفحات بشكل مسبق تلقائيًا.

تجد هذه الميزة في نسخ الإنتاج فقط، إذ لا تحضر Next.js الصفحات مسبقًا في مرحلة التطوير.

router.prefetch(url, as)
  • url: عنوان URL الذي سيُحضر بما في ذلك الوجهات الصريحة (مثل dashboard/) والديناميكية (مثل product/[id]/).
  • as: منسّق اختياري للعنوان url. استُخدم ما قبل الإصدار 9.5.3 في الإحضار المسبق للوجهات الديناميكية.

طريقة استخدامه

لنفترض أنك تملك صفحة تسجيل دخول، ثم توجّه المستخدم بعد تسجيل الدخول إلى لوحة الإعدادات. بإمكانك في هذه الحالة إحضار لوحة الإعدادات مسبقًا كي يكون انتقالك بين الصفحتين أسرع كما في المثال التالي:

import { useCallback, useEffect } from 'react'
import { useRouter } from 'next/router'

export default function Login() {
  const router = useRouter()
  const handleSubmit = useCallback((e) => {
    e.preventDefault()

    fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        /* من البيانات */
      }),
    }).then((res) => {
      // نفِّذ انتقالًا سريعًا في طرف العميل إلى لوحة الإعدادات المُحضرة مسبقًا
  
      if (res.ok) router.push('/dashboard')
    })
  }, [])

  useEffect(() => {
    // الإحضار المسبق للوحة الإعدادات
    router.prefetch('/dashboard')
  }, [])

  return (
    <form onSubmit={handleSubmit}>
      {/* من الحقول */}
      <button type="submit">Login</button>
    </form>
  )
}

التابع router.beforePopState

قد ترغب في بعض الحالات (كاستخدام خادم مخصص) في الإنصات إلى الحدث popstate وتنفيذ شيء ما قبل أن يتعامل معه الموجّه

  • cb: الدالة التي ينبغي تنفيذها ماقبل الحدث popstate. إذ تتلقى هذه الدالة حالة الحدث على شكل كائن له الخواص التالية:
    • url: من النوع String، ويمثّل وجهة الحالة الجديدة، وعادة ما يكون اسم الصفحة.
    • as: من النوع String، ويمثّل العنوان الذي يُعرض ضمن المتصفح.
    • options: من النوع Object، ويضم خيارات إضافية يُرسلها التابع router.push.

إن أعادت الدالة القيمة ، لن يتعامل موجّه Next.js مع popstate، وستكون مسؤولًا عن التعامل معها في هذه الحالة. راجع توثيق تعطيل التوجه باستخدام نظام المفات

طريقة الاستخدام

يمكن استخدام التابع beforePopState لتعديل الطلب أو لتفرض تحديثًا على عملية التصيير من جانب الخادم كما في المثال التالي:

import { useEffect } from 'react'
import { useRouter } from 'next/router'

export default function Page() {
  const router = useRouter()

  useEffect(() => {
    router.beforePopState(({ url, as, options }) => {
      // أريد أن أسمح فقط بهاتين الوجهتين
      if (as !== '/' && as !== '/other') {
        // 404 إجبار الخادم على تصيير الوجهات الخاطئة على شكل صفحة
        window.location.href = as
        return false
      }

      return true
    })
  }, [])

  return <p>Welcome to the page</p>
}

التابع router.back

يتراجع إلى صفحات مخزّنة، ويكافئ النقر على زر "تراجع back" في المتصفح. ينفّذ التابع الأمر ()window.history.back.

طريقة استخدامه

import { useRouter } from 'next/router'

export default function Page() {
  const router = useRouter()

  return (
    <button type="button" onClick={() => router.back()}>
      Click here to go back
    </button>
  )
}

التابع router.reload

يعيد تحميل العنوان الحالي، ويكافئ النقر على زر "تحديث Refresh" في المتصفح. ينفّذ التابع الأمر ()window.location.reload.

طريقة استخدامه

import { useRouter } from 'next/router'

export default function Page() {
  const router = useRouter()

  return (
    <button type="button" onClick={() => router.reload()}>
      Click here to reload
    </button>
  )
}

التابع router.events

بإمكانك الإنصات إلى أحداث مختلفة تجري ضمن موجّه Next.js، وإليك قائمة بالأحداث المدعومة:

  • الحدث routeChangeStart(url, { shallow }): يقع الحدث عندما تبدأ الوجهة بالتغيير.
  • الحدث routeChangeComplete(url, { shallow }): يقع الحدث عندما تتغير الوجهة كليًا.
  • الحدث routeChangeError(err, url, { shallow }): يقع الحدث عندما يحدث خطأ عند تغيير الوجهة، أو ألغي تحميل الوجهة.
    • err.cancelled: يدل على إلغاء عمليلة الانتقال.
  • الحدث beforeHistoryChange(url, { shallow }): يقع الحدث قبل تغيير سجّل المتصفح.
  • الحدث hashChangeComplete(url, { shallow }): يقع الحدث عندما يتغير القسم الفرعي (بعد إشارة #) في الصفحة وليس الصفحة.

ملاحظة: إن url في هذه الأحداث هو ما يظهر ضمن المتصفح مع المسار الجذري basePath ضمنًا

طريقة استخدامه

لكي تنصت مثلًا إلى الحدث routeChangeStart، افتح أو أنشئ الملف pages/_app.js ثم اشترك في الإنصات إلى الحدث كالتالي:

import { useEffect } from 'react'
import { useRouter } from 'next/router'

export default function MyApp({ Component, pageProps }) {
  const router = useRouter()

  useEffect(() => {
    const handleRouteChange = (url, { shallow }) => {
      console.log(
        `App is changing to ${url} ${
          shallow ? 'with' : 'without'
        } shallow routing`
      )
    }

    router.events.on('routeChangeStart', handleRouteChange)
    // `off` ألغ اشتراكك في الإنصات إلى الحدث باستخدام التابع 
    // إن لم يكن المكوّن مثبتًا
    
    return () => {
      router.events.off('routeChangeStart', handleRouteChange)
    }
  }, [])

  return <Component {...pageProps} />
}

استخدمنا تطبيق APP مخصص في مثالنا (pages/_app.js) للإنصات إلى الحدث لأنه سيبقى مثبًتًا أثناء التنقل بين الصفحات، لكن بإمكانك الإنصات إلى أحداث الموجّه في أي مكوّن من مكوّنات التطبيق.

ينبغي تسجيل أحداث الموجّه عندما يُثبّت المكوّن (useEffect أو componentDidMount أو componentWillUnmount) أو إلزاميًا عند وقوع حدث. إن ألغي تحميل الوجهة (بالنقر على رابط مرتين متتاليتين بسرعة مثلًا) سيقع الحدث routeChangeError، وسيضم الوسيط الممر err الخاصية cancelled التي ستحمل القيمة true، كما في المثال التالي:

import { useEffect } from 'react'
import { useRouter } from 'next/router'

export default function MyApp({ Component, pageProps }) {
  const router = useRouter()

  useEffect(() => {
    const handleRouteChangeError = (err, url) => {
      if (err.cancelled) {
        console.log(`Route to ${url} was cancelled!`)
      }
    }

    router.events.on('routeChangeError', handleRouteChangeError)

     // `off` ألغ اشتراكك في الإنصات إلى الحدث باستخدام التابع 
    // إن لم يكن المكوّن مثبتًا
    
    return () => {
      router.events.off('routeChangeError', handleRouteChangeError)
    }
  }, [])

  return <Component {...pageProps} />
}

أخطاء المدقق ESLint المحتملة في Next.js

تُعيد بعض التوابع العائدة للكائن router وعدًا. فإن كنت قد مكّنت القاعدة no-floating-promises في إعدادات ESLint، خذ بعين الاعتبار تعطيلها عمومًا أو فقط لسطر محدد.

فإن كنت تحتاج هذه القاعدة في تطبيقك، لا ينبغي أن يعيد الوعد شيئًا void (إبطال الوعد)، أو يمكنك استخدام دالة غير متزامنة async ثم الانتظار await ليرجع الوعد، ثم إبطال استدعاء الدالة void.

لا ينفع الأسلوبان السابقان إن استُدعيا داخل معالج الأحداث onClick، أما التوابع التي تتأثر فهي:

  • router.push
  • router.replace
  • router.prefetch

الحلول المحتملة

import { useEffect } from 'react'
import { useRouter } from 'next/router'

// يمكنك عنا إحضار وغعادة المستخدم
const useUser = () => ({ user: null, loading: false })

export default function Page() {
  const { user, loading } = useUser()
  const router = useRouter()

  useEffect(() => {
    //عطل التدقيق في السطر التالي
    // eslint-disable-next-line no-floating-promises
    router.push('/login')

    // router.push أبطل الوعد الذي يعيده التابع
      if (!(user || loading)) {
      void router.push('/login')
    }
    // أو استخدم دالة غير متزامنة وانتظر الوعد وأبطل استدعاء الدالة
       async function handleRouteChange() {
      if (!(user || loading)) {
        await router.push('/login')
      }
    }
    void handleRouteChange()
  }, [user, loading])

  return <p>Redirecting...</p>
}

الخطاف withRouter

إن لم يناسبك استخدام useRouter، بإمكنك استخدام الخطاف withRouter الذي يعيد نفس الكائن router لأي مكوّن.

طريق استخدامه

import { withRouter } from 'next/router'

function Page({ router }) {
  return <p>{router.pathname}</p>
}

export default withRouter(Page)

استخدامه مع Typescript

لاستخدام مكوّنات الأصناف مع ، لا بد أن يقبل الصنف خاصيات الموجِّه:

import React from 'react'
import { withRouter, NextRouter } from 'next/router'

interface WithRouterProps {
  router: NextRouter
}

interface MyComponentProps extends WithRouterProps {}

class MyComponent extends React.Component<MyComponentProps> {
  render() {
    return <p>{this.props.router.pathname}</p>
  }
}

export default withRouter(MyComponent)

المصادر

  • الصفحة Next/router من توثيق Next.js الرسمي.