مقالات › React

آموزش React Router؛ مسیرها، Layoutهای تودرتو، پارامترها و صفحات محافظت‌شده

اپلیکیشن چندصفحه‌ای React با React Router بسازید: تعریف مسیرها، Layout تودرتو با Outlet، پارامترهای آدرس، Search Params، ناوبری، مسیرهای محافظت‌شده، بارگذاری تنبل و صفحه‌ی ۴۰۴.

اپلیکیشن‌های تک‌صفحه‌ای (SPA) هم به آدرس نیاز دارند: کاربران صفحه‌ها را نشانه‌گذاری می‌کنند، لینک به اشتراک می‌گذارند و از دکمه‌ی برگشت استفاده می‌کنند؛ موتورهای جستجو هم آدرس‌ها را ایندکس می‌کنند. React Router آدرس‌ها را به کامپوننت‌ها نگاشت می‌کند و ناوبری را آنی نگه می‌دارد. این راهنما همه‌ی چیزهایی را که برای یک اپلیکیشن واقعی لازم دارید پوشش می‌دهد؛ همان الگوهایی که همین وب‌سایت برای مسیرهای دوزبانه‌اش استفاده می‌کند.

راه‌اندازی

npm install react-router-dom
import { BrowserRouter } from "react-router-dom";

createRoot(document.getElementById("root")).render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

BrowserRouter از History API برای آدرس‌های تمیز (/articles/react-router-guide) استفاده می‌کند. سرور شما باید برای مسیرهای ناشناخته index.html را برگرداند تا لینک‌های عمیق بعد از رفرش کار کنند؛ در ASP.NET Core یعنی app.MapFallbackToFile("index.html").

تعریف مسیرها

import { Routes, Route } from "react-router-dom";

export default function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/articles" element={<ArticleList />} />
      <Route path="/articles/:slug" element={<ArticlePage />} />
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
}

React Router مسیرها را بر اساس میزان دقت رتبه‌بندی می‌کند؛ پس ترتیب تعریف مهم نیست و /articles/new خودکار بر /articles/:slug اولویت دارد.

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

بیشتر اپلیکیشن‌ها یک Layout مشترک دارند (هدر، فوتر، سایدبار). مسیرهای تودرتو فرزندان را داخل <Outlet /> والد رندر می‌کنند:

<Routes>
  <Route element={<SiteLayout />}>
    <Route index element={<Home />} />
    <Route path="articles" element={<ArticleList />} />
    <Route path="articles/:slug" element={<ArticlePage />} />
  </Route>

  <Route path="admin" element={<AdminLayout />}>
    <Route index element={<Dashboard />} />
    <Route path="users" element={<Users />} />
  </Route>
</Routes>
import { Outlet } from "react-router-dom";

function SiteLayout() {
  return (
    <>
      <Header />
      <main>
        <Outlet />   {/* مسیر فرزند اینجا رندر می‌شود */}
      </main>
      <Footer />
    </>
  );
}

Layout در حین تغییر فرزندان Mount باقی می‌ماند؛ هدر دوباره رندر نمی‌شود یا نمی‌پرد و Stateهایی مثل باز بودن منو بعد از ناوبری حفظ می‌شوند.

پیشوند زبان به‌عنوان یک Layout

این سایت برای زبان از یک بخش پویا استفاده می‌کند:

<Route path="/:lang" element={<SiteLayout />}>
  <Route index element={<HomePage />} />
  <Route path="articles/:slug" element={<ArticlePage />} />
</Route>

Layout مقدار lang را با useParams() می‌خواند، اعتبارسنجی می‌کند، dir="rtl" یا ltr را تنظیم می‌کند و ترجمه‌ها را به همه‌ی صفحات زیرمجموعه می‌رساند.

پارامترهای آدرس

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

function ArticlePage() {
  const { slug } = useParams();
  const { data, isLoading } = useArticle(slug);
  // ...
}

پارامترها همیشه رشته هستند. آن‌ها را تبدیل و اعتبارسنجی کنید (Number(id)) و حالت پیدا نشدن را درست مدیریت کنید.

Search Params برای فیلتر و صفحه‌بندی

فیلترها جایشان در آدرس است؛ این کار آن‌ها را قابل اشتراک می‌کند و با رفرش از بین نمی‌روند:

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

function ArticleList() {
  const [params, setParams] = useSearchParams();
  const tag = params.get("tag") ?? "";
  const page = Number(params.get("page") ?? 1);

  const selectTag = (next) =>
    setParams((prev) => {
      prev.set("tag", next);
      prev.set("page", "1");
      return prev;
    });

  // /articles?tag=React&page=2
}

برای به‌روزرسانی‌هایی که نباید برای هر کلید یک ورودی در تاریخچه بسازند (مثل تایپ در جستجو)، از { replace: true } استفاده کنید.

ناوبری

import { Link, NavLink, useNavigate } from "react-router-dom";

<Link to="/articles">All articles</Link>

<NavLink to="/articles" className={({ isActive }) => (isActive ? "active" : "")}>
  Articles
</NavLink>

function LoginForm() {
  const navigate = useNavigate();
  async function onSubmit() {
    await login();
    navigate("/dashboard", { replace: true });
  }
}
  • Link برای ناوبری معمولی (هرگز برای لینک داخلی از <a href> ساده استفاده نکنید؛ کل برنامه دوباره بارگذاری می‌شود).
  • NavLink برای منوها وضعیت فعال اضافه می‌کند.
  • useNavigate برای ناوبری بعد از یک عملیات. بعد از ورود از replace استفاده کنید تا دکمه‌ی برگشت به صفحه‌ی ورود برنگردد.

مسیرهای محافظت‌شده

import { Navigate, Outlet, useLocation } from "react-router-dom";

function RequireAuth() {
  const { user } = useAuth();
  const location = useLocation();

  if (!user) {
    return <Navigate to="/login" replace state={{ from: location }} />;
  }
  return <Outlet />;
}

<Route element={<RequireAuth />}>
  <Route path="/dashboard" element={<Dashboard />} />
  <Route path="/settings" element={<Settings />} />
</Route>

بعد از ورود، کاربر را به جایی که از آن آمده برگردانید:

const from = location.state?.from?.pathname ?? "/dashboard";
navigate(from, { replace: true });

هشدار: محافظت مسیرها یک قابلیت تجربه‌ی کاربری است، نه امنیت. API شما باید همچنان هر درخواست را مجوزدهی کند.

بارگذاری تنبل (Lazy Loading) مسیرها

صفحات سنگین را به Bundleهای جداگانه تقسیم کنید تا بارگذاری اول سریع بماند:

import { lazy, Suspense } from "react";

const AdminPanel = lazy(() => import("./admin/AdminPanel"));
const ArticlePage = lazy(() => import("./pages/ArticlePage"));

<Route
  path="articles/:slug"
  element={
    <Suspense fallback={<Spinner />}>
      <ArticlePage />
    </Suspense>
  }
/>

در همین سایت، کتابخانه‌های Markdown و هایلایت کد فقط وقتی بارگذاری می‌شوند که یک مقاله را باز کنید؛ به همین دلیل Bundle صفحه‌ی اصلی حدود ۴۰ درصد کوچک‌تر شده است.

بازگرداندن اسکرول و صفحه‌ی ۴۰۴

در SPAها مرورگر هنگام ناوبری خودکار به بالای صفحه نمی‌رود. با تغییر مسیر اسکرول کنید (یا در Data Routerها از <ScrollRestoration /> استفاده کنید):

function ScrollToTop() {
  const { pathname } = useLocation();
  useEffect(() => window.scrollTo(0, 0), [pathname]);
  return null;
}

یک مسیر path="*" صفحه‌ی ۴۰۴ دوستانه نمایش می‌دهد. اگر HTML را در سرور هم رندر می‌کنید، برای سئو وضعیت واقعی 404 برگردانید.

Data Router (اختیاری)

React Router همچنین createBrowserRouter را همراه با Loader و Action ارائه می‌دهد که قبل از رندر یک مسیر داده را دریافت می‌کنند و ارسال فرم را مدیریت می‌کنند. در React Router v7 این قابلیت به یک حالت فریم‌ورک کامل (جانشین Remix) با رندر سمت سرور تبدیل شده است. برای SPAهای کلاسیک، همان API مبتنی بر کامپوننت <Routes> کاملاً کافی است.

جمع‌بندی

React Router آدرس‌ها را به رابط کاربری تبدیل می‌کند: مسیرها را تعریف کنید، Layoutها را با مسیرهای تودرتو و <Outlet /> به اشتراک بگذارید، از useParams و useSearchParams بخوانید، با Link و useNavigate جابه‌جا شوید، صفحات خصوصی را محافظت و مسیرهای سنگین را تنبل بارگذاری کنید. فیلترها را در آدرس نگه دارید و Fallback سرور را تنظیم کنید تا SPA شما مثل یک وب‌سایت خوش‌ساختار رفتار کند.

پرسش‌های متداول

BrowserRouter یا HashRouter؟

برای آدرس‌های تمیز و مناسب سئو از BrowserRouter استفاده کنید. HashRouter (/#/page) فقط برای هاست‌هایی است که امکان تنظیم Fallback ندارید.

چرا صفحه بعد از رفرش در Production خطای ۴۰۴ می‌دهد؟

سرور در آن مسیر دنبال یک فایل واقعی می‌گردد. آن را طوری تنظیم کنید که برای مسیرهای سمت کلاینت index.html را برگرداند.

چطور داده را بین مسیرها منتقل کنم؟

آدرس را ترجیح دهید (پارامترها و Search Params). برای داده‌ی موقت از state ناوبری و برای داده‌ی مشترک از Context یا کش دریافت داده استفاده کنید.