React Router محبوب‌ترین کتابخانه مسیریابی در React است اما استفاده از آن در کنار برخی از ویژگی‌ها ممکن است کمی پییچده‌تر باشد. در این مقاله قصد داریم تا همه مواردی که باید در مورد React Router بدانیم را باهم بررسی کنیم تا بتوانیم از پیشرفته‌ترین ویژگی‌ها هم به راحتی استفاده کنیم.

مفاهیم اولیه React Router

قبل از شروع به بررسی ویژگی‌های پیشرفته React Router ابتدا می‌خواهیم در مورد اصول اولیه آن صحبت کنیم. برای استفاده از React Router در وب، باید npm i react-router-dom را برای نصب آن اجرا کنیم. این کتابخانه به طور خاص نسخه DOM از React Router را نصب می‌کند. اگر از React Native استفاده می‌کنیم به جای آن باید react-router-nativeرا نصب کنیم. به غیر از این تفاوت کوچک، کتابخانه‌ها تقریباً یکسان کار می‌کنند.

در این مقاله ما روی react-router-domتمرکز خواهیم کرد، اما همانطور که گفتیم هر دو این کتابخانه‌ها تقریباً یکسان هستند.

پس از نصب کتابخانه، برای استفاده از React Router باید سه کار انجام دهیم:

  1. راه‌اندازی Router
  2. تعریف مسیرها
  3. مدیریت navigation

پیکربندی Router

ساده‌ترین مرحله تا کنون راه‌اندازی Router است. تنها کاری که باید انجام دهیم این است که روتر خاصی که نیاز داریم (BrowserRouterبرای وب و NativeRouterبرای موبایل) را وارد کرده و کل برنامه خود را در آن قرار دهیم.

import React from "react"
import ReactDOM from "react-dom/client"
import App from "./App"
import { BrowserRouter } from "react-router-dom"

const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
)

به طور کلی، روتر خود را در صفحه index.jsبرنامه وارد می‌کنیم و کامپوننت Appرا داخل آن قرار می‌دهیم. روتر درست مانند یک context در React کار می‌کند و تمام اطلاعات لازم را در اختیار برنامه ما قرار می‌دهد تا بتوانیم مسیریابی را انجام داده و از تمام هوک‌های سفارشی React Router استفاده کنیم.

تعریف مسیرها

مرحله بعدی در React Router این است که مسیرهای خود را تعریف کنیم. این کار معمولاً در سطح بالاتر برنامه ما انجام می‌شود، مانند کامپوننت App، اما می‌توانیم در هر جایی که بخواهیم این کار را انجام دهیم.

import { Route, Routes } from "react-router-dom"
import { Home } from "./Home"
import { BookList } from "./BookList"

export function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/books" element={<BookList />} />
    </Routes>
  )
}

تعریف مسیرها به سادگی تعریف یک کامپوننت Routeبرای هر مسیر در برنامه خود و سپس قرار دادن تمام آن کامپوننت‌های Routeدر یک کامپوننت Routesاست. زمانی که URL ما تغییر کند، React Router به مسیرهای تعریف شده در کامپوننت Routes نگاه می‌کند و محتوای موجود در پراپ elementمربوط به Route را که pathمطابق با URL دارد رندر می‌کند. در مثال بالا اگر URL ما /booksباشد، در این صورت کامپوننت BookListرندر می‌شود.

نکته خوبی که در مورد React Router وجود دارد این است که وقتی بین صفحات جابه‌جا می‌شویم، فقط محتوای داخل کامپوننت Routes رفرش می‌شود. بقیه مطالب موجود در صفحه یکسان باقی خواهند ماند که این موضوع به عملکرد و تجربه کاربری(UX) بسیار کمک می‌کند.

مدیریت Navigation

آخرین مرحله برای React Router مدیریت navigation است. به طور معمول ما در یک اپلیکیشن با استفاده از تگ‌های anchor بین صفحات جابه‌جا می‌شویم، اما React Router از کامپوننت Linkسفارشی خود برای مدیریت navigation استفاده می‌کند. این کامپوننت فقط یک wrapper در اطراف یک تگ anchor است که برای اطمینان از اینکه همه مسیریابی‌ها و رندر‌های مجدد شرطی به درستی انجام می‌شود مورد استفاده قرار می‌گیرد. بنابراین می‌توانیم از آن همانند یک تگ anchor معمولی استفاده کنیم.

import { Route, Routes, Link } from "react-router-dom"
import { Home } from "./Home"
import { BookList } from "./BookList"

export function App() {
  return (
    <>
      <nav>
        <ul>
          <li><Link to="/">Home</Link></li>
          <li><Link to="/books">Books</Link></li>
        </ul>
      </nav>

      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/books" element={<BookList />} />
      </Routes>
    </>
  )
}

در این مثال ما دو لینک به صفحه home و books اضافه کردیم. همچنین نکته‌ای که وجود دارد این است که ما به جای پراپ hrefکه با تگ anchor مورد استفاده قرار می‌گیرد، از پراپ toبرای تنظیم URL استفاده کردیم. این تنها تفاوت بین کامپوننت Linkو تگ anchor است که باید آن را به خاطر بسپاریم زیرا این احتمال وجود دارد که این دو به اشتباه به‌جای هم استفاده شوند.

نکته دیگری که در مورد کد جدید باید به آن توجه داشته باشیم این است که navigationای که در بالای صفحه خود آن را می‌فرستیم خارج از کامپوننت Routesاست، به این معنی که وقتی صفحه را تغییر می‌دهیم این بخش navigation دوباره رندر نمی‌شود زیرا فقط محتوای موجود در داخل کامپوننت Routes است که با تغییر URL تغییر خواهد کرد.

تعاریف پیشرفته Route

اینجاست که React Router واقعا جالب می‌شود. موارد زیادی وجود دارد که می‌توانیم با کمک آن‌ها مسیریابی انجام دهیم تا مسیرهای پیچیده‌تر خوانایی بالاتر داشته و در کل بسیار کاربردی‌تر شوند. این کار را می‌توانیم از طریق پنج تکنیک اصلی انجام دهیم:

  1. مسیریابی پویا
  2. اولویت مسیریابی
  3. مسیرهای تودرتو
  4. مسیرهای چندگانه
  5. هوک useRoutes

مسیریابی پویا

ساده‌ترین و رایج‌ترین ویژگی پیشرفته در React Router مدیریت مسیرهای پویا است. در مثالی که داریم فرض کنید که می‌خواهیم یک کامپوننت را برای هر Book به شکل منحصربفرد در برنامه خود ارائه دهیم. ما می‌توانیم هر یک از آن مسیرها را هاردکد کنیم اما اگر صدها کتاب داشته باشیم و یا اینکه کاربران توانایی ایجاد کتاب داشته باشند، هاردکد همه این مسیرها غیرممکن است. در چنین حالتی است که به یک مسیر پویا نیاز داریم.

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/books" element={<BookList />} />
  <Route path="/books/:id" element={<Book />} />
</Routes>

مسیر نهایی در مثال بالا یک مسیر پویا است که دارای پارامتر پویا :idمی‌باشد. تعریف مسیرهای پویا در React Router به سادگی قرار دادن یک :در مقابل هر چیزی است که می‌خواهیم قسمت پویا مسیر ما باشد. در مورد مثالی که ما داریم مسیر پویا با هر URLای که با /bookشروع می‌شود و با یک value به پایان می‌رسد مطابقت دارد. برای مثال، //books/1، /books/bookNameو /books/literally-anythingهمگی با مسیر پویا ما مطابقت دارند.

تقریباً همیشه وقتی یک مسیر پویا مانند این مثال داریم و می‌خواهیم به مقدار پویا در کامپوننت سفارشی خود دسترسی پیدا کنیم جایی است که هوک useParamsوارد عمل می‌شود.

import { useParams } from "react-router-dom"

export function Book() {
  const { id } = useParams()

  return (
    <h1>Book {id}</h1>
  )
}

هوک useParams هیچ پارامتری را دریافت نمی‌کند و یک آبجکت با کلیدهایی را برمی‌گرداند که با پارامترهای پویا در مسیر ما مطابقت دارند. در مثال ما پارامتر پویا :idاست، بنابراین هوک useParams یک آبجکت برمی‌گرداند که دارای یک کلید idبوده و مقدار آن کلید، شناسه واقعی در URL خواهد بود. برای مثال، اگر URL ما /books/3بود، صفحه ما Book3 را رندر خواهد کرد.

اولویت مسیریابی

زمانی که ما فقط با مسیرهای دارای هاردکد سروکار داشته باشیم فهمیدن اینکه کدام مسیر رندر می‌شود بسیار آسان است، اما وقتی با مسیرهای پویا کار می‌کنیم این موضوع ممکن است کمی پیچیده‌تر باشد. برای مثال این مسیرها را در نظر بگیرید:

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/books" element={<BookList />} />
  <Route path="/books/:id" element={<Book />} />
  <Route path="/books/new" element={<NewBook />} />
</Routes>

اگر ما URL تحت عنوان /books/newرا داشته باشیم در این صورت با کدام مسیر مطابقت دارد؟ از نظر فنی، ما دو مسیر /books/:idو /books/newرا داریم که باهم مطابقت دارند زیرا مسیر پویا فقط فرض می‌کند که newبخش :idدر URL است، بنابراین React Router به روش دیگری برای تعیین مسیری که باید رندر شود نیاز دارد.

در نسخه‌های قدیمی‌تر React Router، هر مسیری که در ابتدا تعریف شده باشد همان مسیری است که رندر می‌شود، بنابراین در مورد مثالی که داریم مسیر /books/:idرندر خواهد شد که البته بدیهی است این چیزی نیست که ما می‌خواهیم. خوشبختانه نسخه ۶ React Router این را تغییر داد، در نتیجه اکنون React Router از یک الگوریتم برای تعیین مسیری که ما می‌خواهیم استفاده می‌کند. در مورد مثال ما بدیهی است که می‌خواهیم مسیر /books/new را رندر کنیم، بنابراین React Router آن مسیر را انتخاب می‌کند. روش کار این الگوریتم بسیار شبیه به ویژگی CSS است زیرا سعی می‌کند تعیین کند کدام مسیری که با URL ما مطابقت دارد خاص‌ترین است (کم‌ترین تعداد المنت پویا را دارد) و آن مسیر را انتخاب می‌کند.

در همین حین که درمورد اولویت مسیریابی صحبت می‌کنیم می‌خواهم چگونگی ایجاد مسیری که با هر چیزی مطابقت داشته باشد را هم باهم بررسی کنیم:

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/books" element={<BookList />} />
  <Route path="/books/:id" element={<Book />} />
  <Route path="/books/new" element={<NewBook />} />
  <Route path="*" element={<NotFound />} />
</Routes>

یک *با هر چیزی مطابقت دارد که آن را برای مواردی مانند صفحه ۴۰۴ به یک انتخاب عالی تبدیل می‌کند.

مسیرهای تودرتو

درنهایت در این بخش می‌خواهیم با نحوه مدیریت مسیرهای تودرتو آشنا شویم. در مثال بالا ما سه مسیر داریم که با /booksشروع می‌شوند، بنابراین می‌توانیم آن‌ها را به شکل تودرتو در داخل یکدیگر قرار دهیم تا مسیرهای تمیزتری داشته باشیم.

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/books">
    <Route index element={<BookList />} />
    <Route path=":id" element={<Book />} />
    <Route path="new" element={<NewBook />} />
  </Route>
  <Route path="*" element={<NotFound />} />
</Routes>

انجام این کار بسیار ساده است. تنها کار لازم این است که یک Route parent بسازیم که دارای پراپ pathدر مسیر مشترک برای تمام کامپوننت‌های  Routechildها باشد. سپس می‌توانیم تمام کامپوننت‌های Routechild را در داخل Routeparent قرار دهیم. تنها تفاوت این است که پراپ path کامپوننت‌های Routechildها دیگر شامل مسیر مشترک /booksنمی‌شوند. همچنین مسیری که برای /booksوجود داشت با یک کامپوننت Route جایگزین می‌شود که پراپ path ندارد اما در عوض دارای یک پراپ indexمی‌باشد. همه این موارد گویای این هستند که Routeایندکس با Route parent یکی است.

اگر از مسیرهای تودرتو فقط برای انجام این کار استفاده کنیم در این صورت به میزان کمی از مزایای آن بهره‌مند می‌شویم، اما قدرت واقعی مسیرهای تودرتو در نحوه مدیریت Layoutهای مشترک است.

Layout ‌های مشترک

تصور کنید که می‌خواهیم یک بخش nav را با لینک‌هایی به هر کتاب و همچنین لینک‌هایی به فرم کتاب جدید از هر یک از صفحات کتاب خود ارائه کنیم. برای انجام این کار معمولاً باید یک کامپوننت مشترک ایجاد کنیم تا این navigation ذخیره شود و سپس آن را در هر کامپوننت مرتبط با تک‌کتاب import کنیم. این راه کمی گیج‌کننده است، بنابراین React Router راه حل خود را برای این مشکل ایجاد کرده است. اگر یک پراپ elementرا به یک مسیر parent ارسال کنیم، آن کامپوننت برای هر Routechild منفرد ارائه می‌شود، این بدان معناست که می‌توانیم به راحتی یک nav مشترک یا سایر کامپوننت‌های مشترک را در هر صفحه child قرار دهیم.

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/books" element={<BooksLayout />}>
    <Route index element={<BookList />} />
    <Route path=":id" element={<Book />} />
    <Route path="new" element={<NewBook />} />
  </Route>
  <Route path="*" element={<NotFound />} />
</Routes>
import { Link, Outlet } from "react-router-dom"

export function BooksLayout() {
  return (
    <>
      <nav>
        <ul>
          <li><Link to="/books/1">Book 1</Link></li>
          <li><Link to="/books/2">Book 2</Link></li>
          <li><Link to="/books/new">New Book</Link></li>
        </ul>
      </nav>

      <Outlet />
    </>
  )
}

روشی که کد جدید ما کار خواهد کرد به این صورت است که هرگاه مسیری را در داخل Routeparent/bookمطابقت دهیم، کامپوننت BooksLayoutرا که شامل navigation مشترک ما است، رندر می‌کند. سپس هر Route childای که مطابقت داشته باشد، در هر جایی که کامپوننت Outletدر داخل کامپوننت layout قرار می‌گیرد، نمایش داده می‌شود. کامپوننت Outlet اساساً یک کامپوننت placeholder است که محتوای صفحه فعلی ما را رندر می‌کند. این ساختار خیلی مفید است و اشتراک‌گذاری کد بین مسیرها را بسیار آسان می‌کند.

اکنون آخرین روشی که می‌توانیم layoutها را با React Router به اشتراک بگذاریم این است که کامپوننت‌های Route child را داخل Route parent قرار دهیم که به جای پراپ pathفقط یک پراپ elementتعریف می‌کند.

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/books" element={<BooksLayout />}>
    <Route index element={<BookList />} />
    <Route path=":id" element={<Book />} />
    <Route path="new" element={<NewBook />} />
  </Route>
  <Route element={<OtherLayout />}>
    <Route path="/contact" element={<Contact />} />
    <Route path="/about" element={<About />} />
  </Route>
  <Route path="*" element={<NotFound />} />
</Routes>

قسمت کد <Route element={<OtherLayout />}>دو مسیر به نام‌های /contactو /aboutایجاد می‌کند که هر دو در داخل کامپوننت OtherLayoutرندر می‌شوند. اگر بخواهیم آن مسیرها یک layout واحد را به اشتراک بگذارند، حتی اگر مسیر مشابهی نداشته باشند، این تکنیک قرار دادن چندین کامپوننت Routeدر یک کامپوننت Route parent بدون پراپ pathمی‌تواند بسیار مفید باشد.

Outlet Context

آخرین نکته مهمی که باید در مورد کامپوننت‌های Outletبدانیم این است که آن‌ها می‌توانند از یک پراپ contextاستفاده کنند که درست مانند context در React کار می‌کند.

import { Link, Outlet } from "react-router-dom"

export function BooksLayout() {
  return (
    <>
      <nav>
        <ul>
          <li><Link to="/books/1">Book 1</Link></li>
          <li><Link to="/books/2">Book 2</Link></li>
          <li><Link to="/books/new">New Book</Link></li>
        </ul>
      </nav>

      <Outlet context={{ hello: "world" }} />
    </>
  )
}
import { useParams, useOutletContext } from "react-router-dom"

export function Book() {
  const { id } = useParams()
  const context = useOutletContext()

  return (
    <h1>Book {id} {context.hello}</h1>
  )
}

همانطور که در مثال بالا داریم، ما یک مقدار context تحت عنوان { hello: "world" }را ارسال کرده و سپس در کامپوننت child از هوک useOutletContextبرای دسترسی به مقدار context استفاده می‌کنیم. این یک الگوی بسیار رایج است زیرا اغلب ما داده‌های مشترکی را بین تمام کامپوننت‌های child خود خواهیم داشت که استفاده از context می‌تواند بهترین انتخاب برای این کار باشد.

مسیرهای چندگانه

یکی دیگر از کارهای فوق العاده قدرتمندی که می‌توانیم با React Router انجام دهیم، استفاده هم‌زمان از چندین کامپوننت Routesاست. این کار را می‌توانیم به صورت دو کامپوننت جداگانه Routes یا به عنوان Routesهای تودرتو انجام دهیم.

Routesهای جداگانه

اگر می‌خواهیم دو بخش مختلف از محتوای برنامه را رندر کنیم که هر دو به URL برنامه وابسته هستند، به چندین کامپوننت Routes نیاز داریم. یک مثال رایج برای این مفهوم به این صورت است که مثلا ما یک sidebar داریم که می‌خواهیم محتوای خاصی را برای URLهای خاص در آن رندر کنیم و همچنین یک صفحه اصلی که باید محتوای خاصی را براساس URL نشان دهد.

import { Route, Routes, Link } from "react-router-dom"
import { Home } from "./Home"
import { BookList } from "./BookList"
import { BookSidebar } from "./BookSidebar"

export function App() {
  return (
    <>
      <nav>
        <ul>
          <li><Link to="/">Home</Link></li>
          <li><Link to="/books">Books</Link></li>
        </ul>
      </nav>

      <aside>
        <Routes>
          <Route path="/books" element={<BookSidebar />}>
        </Routes>
      </aside>

      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/books" element={<BookList />} />
      </Routes>
    </>
  )
}

در مثال بالا دو Routes داریم.Routes اصلی تمام کامپوننت‌های اصلی صفحه ما را تعریف می‌کند و سپس یک Routes ثانویه در داخل asideداریم که وقتی در /booksهستیم، sidebar صفحه کتاب‌های ما را نمایش می‌دهد. این بدان معناست که اگر URL ما /booksباشد، هر دو کامپوننت Routes محتوایی که دارند را رندر می‌کنند زیرا هر دو در مسیرهای خود مطابقت منحصربهفردی برای /books دارند.

یکی دیگر از کارهایی که می‌توانیم با چندین کامپوننت Routes انجام دهیم، هاردکد کردن پراپ locationاست.

<Routes location="/books">
  <Route path="/books" element={<BookSidebar />}>
</Routes>

با هاردکد کردن پراپ locationرفتار پیش‌فرض یا React Router را نادیده می‌گیریم. بنابراین مهم نیست که URL صفحه ما چیست، این کامپوننت Routesبا Routeخود مطابقت دارد، مثل این است که URL آن /booksمی‌باشد.

Routesهای تودرتو

راه دیگر برای استفاده از چندین کامپوننت Routes این است که آن‌ها را در داخل یکدیگر قرار دهیم. اگر مسیرهای زیادی داریم و می‌خواهیم با انتقال مسیرهای مشابه به فایل‌های خود کدی که داریم را تمیزتر کنیم، این روش بسیار رایج است.

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/books/*" element={<BookRoutes />} />
  <Route path="*" element={<NotFound />} />
</Routes>
import { Routes, Route } from "react-router-dom"
import { BookList } from "./pages/BookList"
import { Book } from "./pages/Book"
import { NewBook } from "./pages/NewBook"
import { BookLayout } from "./BookLayout"

export function BookRoutes() {
  return (
    <Routes>
      <Route element={<BookLayout />}>
        <Route index element={<BookList />} />
        <Route path=":id" element={<Book />} />
        <Route path="new" element={<NewBook />} />
        <Route path="*" element={<NotFound />} />
      </Route>
    </Routes>
  )
}

Routesهای تودرتو در React Router بسیار ساده است. تنها کاری که باید انجام دهیم این است که یک کامپوننت جدید برای ذخیره Routesهای تودرتو ایجاد کنیم، این کامپوننت باید یک کامپوننت Routes داشته باشد و در داخل آن باید تمام کامپوننت‌های Routeای باشند که با Route parent مطابقت دارند. در مثالی که داریم تمام مسیرهای /booksخود را به کامپوننت BookRouteمنتقل می‌کنیم. سپس در Routes parent، باید Routeای را تعریف کنیم که pathآن برابر با pathای که همه Routesهای تودرتو به اشتراک می‌گذارند، باشد. در مثال ما می‌تواند /books باشد. با این حال، نکته مهمی که باید به آن توجه کنیم این است که باید path مربوط به Route parent خود را با یک *پایان دهیم، در غیر این صورت مطابقت با مسیر childها به درستی صورت نمی‌گیرد.

اساساً، کدی که ما نوشته‌ایم می‌گوید زمانی که مسیری با /book/شروع می‌شود، باید داخل کامپوننت BookRoutesجستجو کند تا ببیند آیا Routeای وجود دارد که با آن مطابقت داشته باشد یا خیر. همچنین یک مسیر * دیگر در BookRoutes داریم تا مطمئن شویم که اگر URL ما با هیچ یک از BookRoutes مطابقت نداشته باشد، کامپوننت NotFoundبه درستی رندر خواهد شد.

هوک useRoutes

آخرین چیزی که باید در مورد تعریف مسیرها در React Router بدانیم این است که در صورت تمایل می‌توانیم برای تعریف مسیرهای خود به جای JSX از یک آبجکت جاوااسکریپت استفاده کنیم.

import { Route, Routes } from "react-router-dom"
import { Home } from "./Home"
import { BookList } from "./BookList"
import { Book } from "./Book"

export function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/books">
        <Route index element={<BookList />} />
        <Route path=":id" element={<Book />} />
      </Route>
    </Routes>
  )
}
import { Route, Routes } from "react-router-dom"
import { Home } from "./Home"
import { BookList } from "./BookList"
import { Book } from "./Book"

export function App() {
  const element = useRoutes([
    {
      path: "/",
      element: <Home />
    },
    {
      path: "/books",
      children: [
        { index: true, element: <BookList /> },
        { path: ":id", element: <Book /> }
      ]
    }
  ])

  return element
}

این دو کامپوننت هر دو دقیقاً مسیرهای مشابهی دارند و تنها تفاوت آن‌ها در نحوه تعریف آن‌ها است. اگر بخواهیم که از هوک useRoutesاستفاده کنیم، تمام پراپ‌هایی که معمولاً به کامپوننت‌های Routeارسال می‌کنیم، فقط به‌عنوان جفت key/value ارسال می‌شوند.

مدیریت Navigation

اکنون که می‌دانیم چگونه مسیرهای خود را تعریف کنیم، باید در مورد نحوه جابه‌جایی بین آن‌ها هم صحبت کنیم. این بخش به سه قسمت تقسیم خواهد شد:

  1. Link Navigation
  2. Manual Navigation
  3. Navigation Data

Link Navigation

ابتدا می‌خواهیم درمورد link navigation صحبت کنیم زیرا ساده‌ترین و رایج‌ترین شکل navigation است که با آن روبه‌رو خواهیم شد. ما قبلاً درمورد ابتدایی‌ترین شکل link navigation با استفاده از کامپوننت Linkصحبت کرده‌ایم.

<Link to="/">Home</Link>
<Link to="/books">Books</Link>

این کامپوننت‌های Link می‌توانند کمی پیچیده‌تر شوند. به عنوان مثال می‌توانیم لینک‌های absolute مانند لینک‌های بالا داشته باشیم یا اینکه می‌توانیم لینک‌هایی داشته باشیم که نسبت به کامپوننت فعلی، در حال رندر شدن هستند.

<Link to="/">Home</Link>
<Link to="../">Back</Link>
<Link to="edit">Edit</Link>

برای مثال تصور کنید در مسیر /books/3با لینک‌های بالا هستیم. لینک اول به مسیر /منتهی می‌شود زیرا یک مسیر absolute است. هر مسیری که با یک / شروع می‌شود یک مسیر absolute به‌شمار می‌آید. لینک دوم به مسیر /booksمنتهی می‌شود زیرا یک لینک relative است که از /books/3یک سطح بالاتر رفته و به /booksمی‌رسد. در نهایت، لینک سوم به صفحه /books/3/editمی‌رود، زیرا مسیری را در پراپ toبه انتهای لینک فعلی اضافه می‌کند، زیرا یک لینک relative می‌باشد.

علاوه بر پراپ to، سه پراپ دیگر وجود دارد که برای کامپوننت Linkمهم هستند.

replace

پراپ replaceیک مقدار بولین است که وقتی روی trueتنظیم شود باعث می‌شود این لینک جایگزین صفحه فعلی در تاریخچه مرورگر شود. تصور کنید تاریخچه مرورگر زیر را داریم:

/
/books
/books/3

اگر روی لینکی کلیک کنیم که به صفحه /books/3/editمی‌رود اما دارای ویژگی replaceمی‌باشد که روی true تنظیم شده است، در این حالت تاریخچه جدید ما به این شکل خواهد بود:

/
/books
/books/3/edit

صفحه‌ای که در حال حاضر در آن بودیم با صفحه جدید جایگزین شد. این به این معنی است که اگر روی دکمه بازگشت (Back) در صفحه جدید کلیک کنیم، به جای صفحه /books/3به صفحه /booksبرمی‌گردیم.

reloadDocument

این پراپ یک مقدار بولین دیگر است و  کار با آن بسیار ساده می‌باشد. اگر مقدار آن روی true تنظیم شود، کامپوننت Link مانند یک تگ anchor معمولی عمل می‌کند و به جای اینکه فقط محتوای داخل کامپوننت Routesخود را مجدداً رندر کند، یک صفحه کامل را رفرش می‌کند.

state

پراپ نهایی stateنام دارد. state به ما این امکان را می‌دهد تا داده‌هایی را به همراه Link خود ارسال کنیم که در هیچ کجای URL نمایش داده نمی‌شود. این همان چیزی است که وقتی درمورد navigation data صحبت می‌کنیم به طور عمیق‌تر بررسی خواهیم کرد.

NavLink

المنت بعدی که می‌خواهیم در مورد آن صحبت کنیم کامپوننت NavLinkاست. این کامپوننت دقیقاً مانند کامپوننت Link کار می‌کند، اما به طور خاص برای نشان دادن stateهای فعال روی لینک‌ها، به عنوان مثال در nav barها است. به طور پیش‌فرض اگر ویژگی toیک NavLink با URL صفحه فعلی یکسان باشد لینک یک کلاس activeبه آن اضافه می‌کند که می‌توانیم برای استایل‌سازی از آن استفاده کنیم. اگر این کافی نباشد، می‌توانیم تابعی با پارامتر isActiveرا به classNameیا styleprops یا childهای NavLink ارسال کنیم.

<NavLink
  to="/"
  style={({ isActive }) => ({ color: isActive ? "red" : "black" })}
>
  Home
</NavLink>

NavLink همچنین دارای یک prop به نام endاست که برای کمک به مسیریابی تودرتو مورد استفاده قرار می‌گیرد. به عنوان مثال، اگر در صفحه /books/3هستیم به این معنی است که در حال رندر کردن کامپوننت Bookای هستیم که در مسیر /booksقرار دارد. این بدان معناست که اگر یک NavLink با یک پراپ toتنظیم شده روی /booksداشته باشیم، فعال در نظر گرفته می‌شود. این موضوع به این دلیل است که اگر URL با پراپ to در NavLink مطابقت داشته باشد یا اگر Routeفعلی رندر شده در داخل یک کامپوننت parent باشد که دارای pathای باشد که منطبق بر پراپ NavLink است، در این صورت یک NavLink فعال در نظر گرفته می‌شود. اگر این رفتار پیش‌فرض را نمی‌خواهیم، می‌توانیم پراپ end را روی trueتنظیم کنیم. این کار باعث می‌شود تا URL صفحه دقیقاً با پراپ to در NavLink مطابقت داشته باشد.

Manual Navigation

گاهی اوقات می‌خواهیم براساس مواردی مانند ارسال فرم یا عدم دسترسی به یک صفحه خاص کاربر را به صورت دستی به یک مسیر خاصی هدایت کنیم. در چنین مواقعی باید از کامپوننت Navigateو یا از هوک useNavigationاستفاده کنیم.

کامپوننت Navigate

کامپوننت Navigate یک کامپوننت واقعاً ساده است که وقتی رندر می‌شود کاربر را به طور خودکار به پراپ to از کامپوننت Navigate هدایت می‌کند.

<Navigate to="/" />

این کامپوننت تمام ویژگی‌های کامپوننت Linkرا به اشتراک می‌گذارد بنابراین به راحتی می‌توانیم آن را به to، replaceو stateارسال کنیم.

هوک useNavigation

استفاده از هوک useNavigation بسیار ساده است زیرا هیچ پارامتری را دریافت نمی‌کند و یک تابع navigateرا برمی‌گرداند که می‌توانیم از آن برای هدایت کاربر به صفحات خاص استفاده کنیم. این تابع navigate دو پارامتر دارد. پارامتر اول لوکیشین toاست که می‌خواهیم کاربر را به آن هدایت کنیم و پارامتر دوم یک آبجکت است که می‌تواند کلیدهایی برای replace و state داشته باشد.

const navigate = useNavigate()

function onSubmit() {
  // Submit form results
  navigate("/books", { replace: true, state: { bookName: "Fake Title" }})
}

کد بالا کاربر را به مسیر /booksهدایت می‌کند. همچنین جایگزین مسیر فعلی در تاریخچه مرورگر خواهد شد و برخی از اطلاعات state را نیز منتقل خواهد کرد.

راه دیگری که می‌توانیم از تابع navigateاستفاده کنیم ارسال یک عدد به آن است. این کار به ما این امکان را می‌دهد تا کلیک کردن روی باتن forward/back را شبیه سازی کنیم.

navigate(-1) // Go back one page in history
navigate(-3) // Go back three pages in history
navigate(1) // Go forward one page in history

Navigation Data

درنهایت وقت آن است که در مورد انتقال داده‌ها بین صفحات صحبت کنیم. ۳ روش اصلی برای انتقال داده بین صفحات وجود دارد که عبارتند از:

  1. پارامترهای پویا
  2. پارامترهای جستجو
  3. داده‌های State/Location

پارامترهای پویا

قبلاً در مورد نحوه استفاده از پارامترهای پویا در URLها با استفاده از هوک useParamsصحبت کرده‌ایم. این بهترین راه برای مدیریت اطلاعات ارسالی مانند idها به‌شمار می‌آید.

پارامترهای جستجو

پارامترهای جستجو همه پارامترهایی هستند که بعد از ?در یک URL قرار می‌گیرند، مثلا  (?name=Kyle&age=27). برای کار با پارامترهای جستجو، باید از هوک useSearchParamsاستفاده کنیم که بسیار شبیه به هوک useStateعمل می‌کند.

import { useSearchParams } from "react-router-dom"

export function SearchExample() {
  const [searchParams, setSearchParams] = useSearchParams({ n: 3 })
  const number = searchParams.get("n")

  return (
    <>
      <h1>{number}</h1>
      <input
        type="number"
        value={number}
        onChange={e => setSearchParams({ n: e.target.value })}
      />
    </>
  )
}

در این مثال، یک مقدار input داریم که با تایپ کردن، بخش جستجوی URL ما به‌روزرسانی می‌شود. به عنوان مثال، اگر ورودی ما دارای مقدار ۳۲ باشد URL ما شبیه http://localhost:3000?n=32خواهد بود. هوک useSearchParamsهمانند هوک useStateیک مقدار اولیه می‌گیرد که در مورد مثال ما این مقدار اولیه nروی ۳ تنظیم شده است. این هوک سپس دو مقدار را برمی‌گرداند. مقدار اول تمام پارامترهای جستجوی ما است و مقدار دوم تابعی برای به‌روزرسانی پارامترهای جستجوی می‌باشد. تابع set فقط یک آرگومان می‌گیرد که مقدار جدید پارامترهای جستجوی ما است. اولین مقداری که شامل پارامترهای جستجو می‌باشد کمی گیج‌کننده به نظر می‌رسد زیرا این مقدار از نوع URLSearchParamsمی‌باشد. به همین دلیل است که در خط ۵ مثال بالا باید از دستور .getاستفاده کنیم.

داده‌های State/Location

آخرین نوع داده‌ای که می‌توانیم آن را ذخیره کنیم داده‌های state و location است. همه این اطلاعات از طریق هوک useLocationقابل دسترسی هستند. استفاده از این هوک بسیار ساده است زیرا فقط یک مقدار برمی‌گرداند و هیچ پارامتری را نمی‌گیرد.

const location = useLocation()

اگر آدرس http://localhost/books?n=32#idرا داشته باشیم، مقدار بازگشتی هوک useLocationبه این شکل خواهد بود:

{
  pathname: "/books",
  search: "?n=32",
  hash: "#id",
  key: "2JH3G3S",
  state: null
}

این آبجکت location شامل تمام اطلاعات مربوط به URL ما می‌باشد. همچنین حاوی یک کلید منحصربه‌فرد است که اگر بخواهیم اطلاعات را برای زمانی که کاربر روی دکمه Back کلیک می‌کند تا به صفحه بازگردد ذخیره کنیم، می‌توانیم آن را برای ذخیره‌سازی کش مورد استفاده قرار دهیم. همینطور یک ویژگی State داریم که از هوک useLocation بازگردانده شده است. این داده state می‌تواند هر چیزی باشد و بدون ذخیره شدن در URL بین صفحات ارسال می‌شود. برای مثال اگر روی Linkکلیک کنیم که به شکل زیر است:

<Link to="/books" state={{ name: "Kyle" }}>

در این صورت مقدار state در آبجکت location روی { name: "Kyle" }تنظیم می‌شود.

به عنوان مثال اگر بخواهیم بین صفحاتی که نباید در URL ذخیره شوند پیام‌های ساده‌ای ارسال کنیم استفاده از این روش می‌تواند بسیار مفید باشد. یک مثال خوب برای این موضوع می‌تواند چیزی شبیه پیام موفقیت‌آمیز بودن ایجاد کتاب جدید باشد.

Routers In Depth

تاکنون در این مقاله حدود ۹۵٪ از آنچه که باید در مورد React Router بدانیم را باهم بررسی کردیم، اما این کتابخانه هنوز مفاهیمی دارد که باید به آن‌ها نیز بپردازیم. در بخش مفاهیم اولیه در مورد تعریف router صحبت کرده و به BrowserRouterو NativeRouterاشاره کردیم، اما این دو تنها روترهای موجود نیستند. در واقع ۶ روتر وجود دارد که در این بخش هر یک از آن‌ها را به طور کامل بررسی خواهیم کرد.

BrowserRouter

این روتر پیش‌فرضی است که اگر روی یک وب‌ اپلیکیشن کار می‌کنیم باید از آن استفاده کنیم. BrowserRouter روتری است که در ۹۹٪ از همه برنامه‌هایی که داریم مورد استفاده قرار خواهد گرفت، زیرا همه موارد استفاده از مسیریابی معمولی را پوشش می‌دهد. هر یک از روترهای دیگری که در مورد آن‌ها صحبت خواهیم کرد موارد استفاده مخصوص خود را دارند، بنابراین اگر با این موارد استفاده مناسب برنامه ما نباشند، BrowserRouter همان چیزی است که باید آن را انتخاب کنیم.

NativeRouter

NativeRouter در اصل معادل BrowserRouter است اما برای React Native اختصاص دارد. اگر از React Native استفاده می‌کنیم باید این روتر را به کار بگیریم.

HashRouter

این روتر بسیار شبیه به BrowserRouter کار می‌کند، اما تفاوت اصلی این است که به جای اینکه URL را به چیزی مانند http://localhost:3000/booksتغییر دهد، آن را مانند http://localhost:3000/#/booksدر هش ذخیره می‌کند. همانطور که می‌بینیم این URL دارای یک #بعد از URL است که نشان‌دهنده بخش هش URL می‌باشد. هر چیزی در این بخش فقط اطلاعات اضافی است که معمولاً یک id در صفحه را برای اهداف مربوط به اسکرول کردن نشان می‌دهد. زیرا یک صفحه به طور خودکار به المنت با id نشان داده شده توسط هش هنگام لود شدن صفحه اسکرول می‌شود.

در React Router این هش در واقع برای ذخیره اطلاعات id برای اسکرول استفاده نمی‌شود، بلکه اطلاعات مربوط به URL فعلی را ذخیره می‌کند. دلیل اینکه React Router این کار را انجام می‌دهد این است که برخی از ارائه‌دهندگان هاست به ما اجازه نمی‌دهند تا URL صفحه خود را تغییر دهیم. ما در این شرایط بسیار نادر می‌خواهیم از HashRouterاستفاده کنیم زیرا این روتر URL واقعی صفحه ما را تغییر نمی‌دهد و فقط هش صفحه را عوض می‌کند. اگر می‌توانید از هر URLای استفاده کنیم نباید این روتر را در برنامه‌های خود به کار بگیریم.

HistoryRouter

HistoryRouter(که در حال حاضر unstable_HistoryRouterنامیده می‌شود) روتری است که به ما این امکان را می‌دهد که به صورت دستی آبجکت history‌ای را که React Router برای ذخیره تمام اطلاعات مربوط به تاریخچه مسیریابی برنامه استفاده می‌کند، کنترل کنیم. این آبجکت history کمک می‌کند تا مطمئن شویم مواردی مانند باتن back و forward در مرورگر به درستی کار می‌کنند.

این روتری است که احتمالاً هرگز نباید از آن استفاده کنیم، مگر اینکه دلیل بسیار خاصی داشته باشیم که بخواهیم رفتار تاریخچه پیش‌فرض React Router را بازنویسی یا کنترل کنیم.

MemoryRouter

MemoryRouterکمی متفاوت از بقیه روترهایی است که در مورد آن‌ها صحبت کردیم، زیرا به جای ذخیره اطلاعات درمورد مسیر فعلی در URL مرورگر، اطلاعات مسیریابی را مستقیماً در حافظه ذخیره می‌کند. بدیهی است که استفاده از این روتر برای عملیات مسیریابی معمولی انتخاب درستی نیست اما استفاده از آن برای زمانی که در حال نوشتن تست‌هایی برای برنامه خود هستیم که به مرورگر دسترسی ندارند، می‌تواند بسیار مفید باشد.

به دلیل نحوه عملکرد React Router ما باید کامپوننت‌های خود را داخل روتر قرار دهیم، در غیر این صورت تمام کد مسیریابی ما دچار خطا می‌شود. به این معنی که حتی اگر می‌خواهیم یک کامپوننت را تست کنیم، باید آن را داخل یک روتر قرار دهیم وگرنه خطاهایی ایجاد می‌کند. اگر کد خود را به گونه‌ای تست می‌کنیم که به مرورگر دسترسی نداشته باشد (مانند unit testing)، روترهایی که تاکنون در مورد آن‌ها صحبت کرده‌ایم دچار خطا می‌شوند زیرا همه آن‌ها برای URL به مرورگر وابسته هستند. از طرف دیگر MemoryRouter تمام اطلاعات خود را در حافظه ذخیره می‌کند به این معنی که هرگز به مرورگر دسترسی پیدا نمی‌کند. درنتیجه زمانی که برای تست کردن کامپوننت‌های خود از unit testing استفاده می‌کنیم این روتر یک انتخاب ایده‌آل به شمار می‌آید. به غیر از این مورد خاص، MemoryRouter هرگز استفاده نمی‌شود.

StaticRouter

روتر نهایی StaticRouterاست. این روتر به‌طور خاص برای سروری مورد استفاده قرار می‌گیرد که برنامه‌های React ما را رندر می‌کند، زیرا یک پراپ locationرا می‌گیرد و برنامه ما را با استفاده از آن location به عنوان URL نمایش می‌دهد. این روتر در واقع نمی‌تواند هیچ مسیریابی انجام دهد و فقط یک صفحه استاتیک را ارائه می‌دهد، اما برای رندر سرور یک انتخاب عالی محسوب می‌شود زیرا ما می‌خواهیم که فقط HTML برنامه خود را روی سرور رندر کنیم و سپس کلاینت می‌تواند تمام مسیریابی‌های ما و موارد دیگر را تنظیم کند.

<StaticRouter location="/books">
  <App />
</StaticRouter>

جمع‌بندی

React Router یک کتابخانه بسیار بزرگ با هزاران ویژگی شگفت‌انگیز است و به همین دلیل است که اکثر افراد از این کتابخانه برای مسیریابی در برنامه‌های خود استفاده می‌کنند.