Next.js/next router
ننصحك قبل قراءة المقال بالاطلاع على التوجّه في 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
يبقى نفسه.
إن لم ترغب بهذا السلوك، لديك خياران:
- تأكد من تحديث كل حالة يدويًا باستخدام الخطاف
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 الرسمي.