React Router چیست؟ آموزش کامل مسیریابی در React با ساخت داشبورد هوش مصنوعی

در این آموزش React Router را از صفر یاد می‌گیرید؛ از BrowserRouter، Route و Link تا Layout، Outlet، مسیر پویا، Query String، صفحه 404 و Protected Route. در پایان یک داشبورد هوش مصنوعی چندصفحه‌ای متصل به API درواره می‌سازیم.

Share
React Router چیست؟ آموزش کامل مسیریابی در React با ساخت داشبورد هوش مصنوعی

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

اگر برای جابه‌جایی میان این بخش‌ها از لینک معمولی HTML استفاده کنیم، مرورگر هر بار کل صفحه را از سرور دریافت و برنامه React را از ابتدا اجرا می‌کند. React Router امکان می‌دهد URL مرورگر را به Componentهای React متصل کنیم و مسیریابی سمت Client را بدون Reload کامل صفحه انجام دهیم.

اما React Router دقیقاً چیست؟ چه تفاوتی میان react-router و react-router-dom وجود دارد؟ BrowserRouter، Routes، Route و Outlet چه می‌کنند؟ چگونه مسیر پویا، Query Parameter، صفحه 404 و مسیر محافظت‌شده بسازیم؟ چرا پس از انتشار، Refresh کردن یک Route ممکن است خطای 404 ایجاد کند؟

در این راهنما React Router را از مفاهیم پایه تا ساخت یک داشبورد هوش مصنوعی واقعی بررسی می‌کنیم.

React Router چیست؟

React Router یک کتابخانه مسیریابی برای برنامه‌های React است. این کتابخانه URL مرورگر را با رابط کاربری برنامه هماهنگ می‌کند.

برای مثال:

/                  صفحه اصلی
/playground        محیط آزمایش هوش مصنوعی
/models            فهرست مدل‌ها
/history           تاریخچه درخواست‌ها
/conversations/42  جزئیات مکالمه شماره 42
/settings          تنظیمات

React Router بررسی می‌کند URL فعلی با کدام Route مطابقت دارد و سپس Component مربوط را نمایش می‌دهد.

برای مثال:

<Routes>
  <Route path="/" element={<HomePage />} />
  <Route path="/models" element={<ModelsPage />} />
  <Route path="/settings" element={<SettingsPage />} />
</Routes>

طبق مستندات رسمی React Router، Routeها با اتصال Segmentهای URL به Elementهای رابط کاربری تعریف می‌شوند.

چرا در React به Router نیاز داریم؟

بدون Router ممکن است برای نمایش صفحات از State استفاده کنید:

function App() {
  const [page, setPage] = useState("home");

  if (page === "models") {
    return <ModelsPage />;
  }

  if (page === "settings") {
    return <SettingsPage />;
  }

  return <HomePage />;
}

این روش برای یک نمونه بسیار کوچک کار می‌کند، اما مشکلات مهمی دارد:

  • URL تغییر نمی‌کند.
  • کاربر نمی‌تواند آدرس صفحه را Bookmark کند.
  • دکمه Back و Forward مرورگر رفتار طبیعی ندارد.
  • اشتراک‌گذاری لینک یک صفحه ممکن نیست.
  • Refresh صفحه وضعیت فعلی را از بین می‌برد.
  • مسیرهای تو‌در‌تو پیچیده می‌شوند.
  • مدیریت Parameterها دشوار است.
  • نمایش Layout مشترک تکراری می‌شود.

React Router این نیازها را در یک ساختار مسیریابی استاندارد مدیریت می‌کند.

مسیریابی سمت Client چیست؟

در یک وب‌سایت سنتی، کلیک روی هر لینک معمولاً Request جدیدی به سرور می‌فرستد:

Browser -> Server -> HTML جدید

در یک Single Page Application یا SPA، فایل اصلی برنامه یک‌بار بارگذاری می‌شود و Router بخش مناسب رابط کاربری را براساس URL نمایش می‌دهد:

Browser
   |
   v
React Application
   |
   v
React Router
   |
   v
Component مربوط به URL

تغییر Route می‌تواند بدون Reload کامل صفحه انجام شود؛ اما ممکن است Component جدید همچنان برای دریافت داده به Backend درخواست بفرستد.

React Router چه کاری انجام نمی‌دهد؟

React Router مسئول مسیریابی رابط کاربری است. این کتابخانه جایگزین موارد زیر نیست:

  • Backend
  • دیتابیس
  • احراز هویت سمت سرور
  • کنترل واقعی دسترسی
  • مدیریت API Key
  • مدیریت State عمومی
  • دریافت خودکار تمام داده‌ها در هر Mode
  • Server-side Rendering در همه تنظیمات
  • مجوزدهی سازمانی
  • Rate Limiting

به‌خصوص باید بدانید Protected Route در Frontend یک مرز امنیتی واقعی نیست. Backend همچنان باید دسترسی کاربر را برای هر Request بررسی کند.

تفاوت react-router و react-router-dom چیست؟

در نسخه‌های قدیمی‌تر، بسیاری از آموزش‌ها Package زیر را نصب می‌کردند:

npm install react-router-dom

به همین دلیل عبارت react-router-dom هنوز در جست‌وجوها، پروژه‌های موجود و آموزش‌های اینترنتی بسیار دیده می‌شود.

در مستندات نسخه فعلی React Router، برای Declarative Mode از Package زیر استفاده می‌شود:

npm install react-router

و Componentهایی مانند BrowserRouter مستقیماً از آن Import می‌شوند:

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

راهنمای نصب فعلی در مستندات رسمی React Router همین الگو را ارائه می‌کند.

اگر پروژه موجود شما از react-router-dom استفاده می‌کند، بدون بررسی نسخه و Migration Guide نام Packageها و Importها را تغییر ندهید. ابتدا نسخه نصب‌شده را ببینید:

npm list react-router react-router-dom

سپس مستندات مربوط به همان نسخه را بررسی کنید.

Modeهای مختلف React Router

React Router در نسخه‌های جدید چند شیوه استفاده دارد:

  • Declarative Mode
  • Data Mode
  • Framework Mode

Declarative Mode

در این روش Routeها با Componentهایی مانند BrowserRouter، Routes و Route تعریف می‌شوند:

<BrowserRouter>
  <Routes>
    <Route path="/" element={<Home />} />
  </Routes>
</BrowserRouter>

این روش برای یادگیری مفاهیم اصلی و بسیاری از SPAهای React مناسب است.

Data Mode

در Data Mode معمولاً Router با توابعی مانند createBrowserRouter ساخته می‌شود. این Mode قابلیت‌هایی مانند Loader، Action، Pending UI و Error Handling مبتنی بر Route را در اختیار برنامه قرار می‌دهد.

Framework Mode

Framework Mode مجموعه گسترده‌تری از قابلیت‌های Routing، Data Loading، Rendering، Route Module و Deployment را فراهم می‌کند.

در این مقاله برای شفافیت و کاربرد عمومی، ابتدا Declarative Mode را آموزش می‌دهیم و در بخش جداگانه نمونه‌ای از Data Router نیز ارائه می‌کنیم.

پیش‌نیازها

برای دنبال کردن پروژه به این موارد نیاز دارید:

  • Node.js سازگار با Vite و React Router
  • npm یا Package Manager مشابه
  • آشنایی مقدماتی با JavaScript
  • آشنایی پایه با React Component و Hook
  • یک ویرایشگر مانند VS Code

نسخه‌های نصب‌شده را بررسی کنید:

node --version
npm --version

ساخت پروژه React با Vite

یک پروژه جدید ایجاد کنید:

npm create vite@latest react-router-ai-dashboard -- --template react

وارد پوشه شوید:

cd react-router-ai-dashboard

وابستگی‌ها را نصب کنید:

npm install

React Router را نصب کنید:

npm install react-router

Development Server را اجرا کنید:

npm run dev

اضافه کردن BrowserRouter

فایل src/main.jsx:

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router";
import App from "./App.jsx";
import "./index.css";

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

BrowserRouter از History API مرورگر برای هماهنگ کردن URL و رابط کاربری استفاده می‌کند.

تمام Componentهایی که از Hookها یا Componentهای React Router استفاده می‌کنند باید داخل Router قرار داشته باشند.

این ساختار اشتباه است:

function App() {
  const navigate = useNavigate();

  return (
    <BrowserRouter>
      <Home />
    </BrowserRouter>
  );
}

در این مثال useNavigate پیش از قرار گرفتن Component در Context مربوط به Router اجرا شده است.

ساختار درست:

<BrowserRouter>
  <App />
</BrowserRouter>

و سپس داخل App:

function App() {
  const navigate = useNavigate();

  return <button onClick={() => navigate("/")}>خانه</button>;
}

تعریف اولین Route

فایل src/App.jsx:

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

function HomePage() {
  return <h1>صفحه اصلی</h1>;
}

function ModelsPage() {
  return <h1>مدل‌های هوش مصنوعی</h1>;
}

function SettingsPage() {
  return <h1>تنظیمات</h1>;
}

export default function App() {
  return (
    <Routes>
      <Route path="/" element={<HomePage />} />
      <Route path="/models" element={<ModelsPage />} />
      <Route path="/settings" element={<SettingsPage />} />
    </Routes>
  );
}

با رفتن به آدرس /models، Component مربوط به ModelsPage نمایش داده می‌شود.

Routes و Route چه تفاوتی دارند؟

Routes

Routes مجموعه Routeها را بررسی می‌کند و بهترین Route مطابق با URL فعلی را نمایش می‌دهد.

<Routes>
  ...
</Routes>

Route

هر Route یک Path را به یک Element متصل می‌کند:

<Route
  path="/models"
  element={<ModelsPage />}
/>

ویژگی path مسیر و element رابط کاربری مربوط به آن مسیر را مشخص می‌کند.

برای جابه‌جایی داخلی در برنامه از Link استفاده کنید:

import { Link } from "react-router";

export default function Navigation() {
  return (
    <nav>
      <Link to="/">خانه</Link>
      <Link to="/models">مدل‌ها</Link>
      <Link to="/settings">تنظیمات</Link>
    </nav>
  );
}

استفاده از Anchor معمولی:

<a href="/models">مدل‌ها</a>

ممکن است باعث Reload کامل Document شود.

برای لینک داخلی برنامه:

<Link to="/models">مدل‌ها</Link>

برای لینک خارجی همچنان Anchor معمولی مناسب است:

<a
  href="https://darvareh.ir/models"
  target="_blank"
  rel="noreferrer"
>
  مشاهده مدل‌های درواره
</a>

NavLink می‌تواند تشخیص دهد لینک فعلی Active است:

import { NavLink } from "react-router";

export default function Sidebar() {
  return (
    <nav>
      <NavLink
        to="/"
        end
        className={({ isActive }) =>
          isActive ? "nav-link active" : "nav-link"
        }
      >
        داشبورد
      </NavLink>

      <NavLink
        to="/playground"
        className={({ isActive }) =>
          isActive ? "nav-link active" : "nav-link"
        }
      >
        Playground
      </NavLink>

      <NavLink
        to="/models"
        className={({ isActive }) =>
          isActive ? "nav-link active" : "nav-link"
        }
      >
        مدل‌ها
      </NavLink>
    </nav>
  );
}

ویژگی end روی Route ریشه مهم است. بدون آن ممکن است لینک / برای مسیرهای دیگر نیز Active باقی بماند.

CSS:

.nav-link {
  display: block;
  padding: 10px 14px;
  border-radius: 10px;
  color: #566076;
  text-decoration: none;
}

.nav-link:hover {
  background: #f1f3fb;
}

.nav-link.active {
  color: #fff;
  background: #6046d7;
}

ساخت Nested Routes

Nested Route یا Route تو‌در‌تو زمانی کاربرد دارد که چند صفحه Layout مشترک داشته باشند.

برای مثال:

/dashboard
/dashboard/usage
/dashboard/settings

تعریف Routeها:

<Routes>
  <Route path="/dashboard" element={<DashboardLayout />}>
    <Route index element={<DashboardHome />} />
    <Route path="usage" element={<UsagePage />} />
    <Route path="settings" element={<SettingsPage />} />
  </Route>
</Routes>

مسیرهای فرزند به‌صورت خودکار Path والد را دریافت می‌کنند:

/dashboard
/dashboard/usage
/dashboard/settings

Outlet چیست؟

Outlet محل نمایش Route فرزند در Layout والد است.

import { NavLink, Outlet } from "react-router";

export default function DashboardLayout() {
  return (
    <div className="dashboard">
      <aside>
        <NavLink to="/dashboard" end>
          نمای کلی
        </NavLink>

        <NavLink to="/dashboard/usage">
          مصرف
        </NavLink>

        <NavLink to="/dashboard/settings">
          تنظیمات
        </NavLink>
      </aside>

      <main>
        <Outlet />
      </main>
    </div>
  );
}

اگر URL برابر /dashboard/usage باشد، UsagePage در محل Outlet نمایش داده می‌شود.

براساس راهنمای Nested Routes در React Router، Route فرزند در Outlet مربوط به Route والد Render می‌شود.

Index Route چیست؟

Index Route صفحه پیش‌فرض یک Route والد است:

<Route path="/dashboard" element={<DashboardLayout />}>
  <Route index element={<DashboardHome />} />
  <Route path="usage" element={<UsagePage />} />
</Route>

وقتی کاربر وارد /dashboard شود، DashboardHome در Outlet نمایش داده می‌شود.

Index Route نمی‌تواند Route فرزند داشته باشد. اگر به ساختار تو‌در‌تو نیاز دارید، از Layout Route استفاده کنید.

Layout Route چیست؟

Route بدون path می‌تواند فقط یک Layout مشترک ایجاد کند:

<Routes>
  <Route element={<PublicLayout />}>
    <Route path="/" element={<HomePage />} />
    <Route path="/about" element={<AboutPage />} />
  </Route>

  <Route element={<DashboardLayout />}>
    <Route path="/dashboard" element={<DashboardPage />} />
    <Route path="/settings" element={<SettingsPage />} />
  </Route>
</Routes>

Layout Route Segment جدیدی به URL اضافه نمی‌کند.

مسیر پویا یا Dynamic Route

اگر بخشی از Path با : شروع شود، یک Dynamic Segment است.

<Route
  path="/conversations/:conversationId"
  element={<ConversationPage />}
/>

این Route با آدرس‌های زیر مطابقت دارد:

/conversations/1
/conversations/42
/conversations/abc-123

برای خواندن Parameter از useParams استفاده کنید:

import { useParams } from "react-router";

export default function ConversationPage() {
  const { conversationId } = useParams();

  return (
    <section>
      <h1>جزئیات مکالمه</h1>
      <p>شناسه: {conversationId}</p>
    </section>
  );
}

Dynamic Segmentها در مستندات Routing توضیح داده شده‌اند.

اعتبارسنجی Route Parameter

مقادیر useParams از URL می‌آیند و قابل اعتماد نیستند. اگر شناسه باید عدد باشد، آن را بررسی کنید:

import { useParams } from "react-router";

export default function ConversationPage() {
  const { conversationId } = useParams();
  const numericId = Number(conversationId);

  if (!Number.isInteger(numericId) || numericId <= 0) {
    return <p>شناسه مکالمه معتبر نیست.</p>;
  }

  return <p>مکالمه شماره {numericId}</p>;
}

Backend نیز باید شناسه و دسترسی کاربر را مستقل از Frontend اعتبارسنجی کند.

چند Parameter در یک Route

<Route
  path="/workspaces/:workspaceId/conversations/:conversationId"
  element={<ConversationPage />}
/>

خواندن مقادیر:

const {
  workspaceId,
  conversationId,
} = useParams();

URL نمونه:

/workspaces/team-a/conversations/42

Optional Segment

در نسخه‌هایی که از Optional Segment پشتیبانی می‌کنند، می‌توان علامت ? را به Segment اضافه کرد:

<Route
  path="/reports/:reportId?"
  element={<ReportsPage />}
/>

این Route ممکن است با هر دو مسیر مطابقت داشته باشد:

/reports
/reports/42

قبل از استفاده، پشتیبانی آن را در نسخه نصب‌شده بررسی کنید.

Splat Route

برای تطبیق ادامه مسیر می‌توان از * استفاده کرد:

<Route
  path="/files/*"
  element={<FileBrowserPage />}
/>

مثال:

/files/documents/2026/report.pdf

Splat برای File Browser، Documentation و بعضی ساختارهای تو‌در‌تو کاربرد دارد؛ اما استفاده بیش از حد از آن می‌تواند Routeها را مبهم کند.

صفحه 404 در React Router

در انتهای Routeها یک Catch-all Route قرار دهید:

<Route path="*" element={<NotFoundPage />} />

Component صفحه 404:

import { Link } from "react-router";

export default function NotFoundPage() {
  return (
    <main>
      <p>خطای 404</p>
      <h1>صفحه موردنظر پیدا نشد</h1>

      <Link to="/">
        بازگشت به صفحه اصلی
      </Link>
    </main>
  );
}

نکته مهم: این صفحه 404 مربوط به Router سمت Client است. اگر Web Server پیش از اجرای React برای Route ناشناخته 404 برگرداند، باید Rewrite سرور نیز تنظیم شود.

جابه‌جایی برنامه‌نویسی‌شده با useNavigate

گاهی Navigation بعد از یک عملیات انجام می‌شود:

  • بعد از ورود موفق
  • بعد از ساخت Resource
  • بعد از حذف آیتم
  • پس از ثبت تنظیمات
  • بعد از انتخاب Workspace

مثال:

import { useNavigate } from "react-router";

export default function CreateConversationButton() {
  const navigate = useNavigate();

  async function handleCreate() {
    const conversation = await createConversation();

    navigate(
      `/conversations/${conversation.id}`
    );
  }

  return (
    <button
      type="button"
      onClick={handleCreate}
    >
      مکالمه جدید
    </button>
  );
}

بازگشت به صفحه قبل:

navigate(-1);

رفتن به صفحه بعد در History:

navigate(1);

جایگزین کردن Entry فعلی:

navigate("/dashboard", {
  replace: true,
});

گزینه replace برای مواردی مانند Redirect بعد از Login مفید است تا کاربر با دکمه Back به صفحه Login برنگردد.

از Link برای Navigation قابل‌مشاهده و قابل‌کلیک استفاده کنید:

<Link to="/models">
  مشاهده مدل‌ها
</Link>

از useNavigate زمانی استفاده کنید که Navigation نتیجه یک رویداد یا عملیات است:

await saveSettings();
navigate("/dashboard");

برای تبدیل تمام لینک‌ها به Button و useNavigate دلیل خوبی وجود ندارد. Link از نظر ساختار HTML، دسترسی‌پذیری و رفتار طبیعی لینک مناسب‌تر است.

برای Redirect در Render:

import { Navigate } from "react-router";

function LegacyModelsPage() {
  return (
    <Navigate
      to="/models"
      replace
    />
  );
}

این روش برای Routeهای قدیمی یا Redirect شرطی قابل‌استفاده است.

استفاده از useLocation

useLocation اطلاعات URL فعلی را برمی‌گرداند:

import { useLocation } from "react-router";

export default function CurrentLocation() {
  const location = useLocation();

  return (
    <pre>
      {JSON.stringify(location, null, 2)}
    </pre>
  );
}

مقادیر متداول:

location.pathname
location.search
location.hash
location.state
location.key

نمونه ثبت تغییر Route:

import { useEffect } from "react";
import { useLocation } from "react-router";

export default function RouteTracker() {
  const location = useLocation();

  useEffect(() => {
    console.log("Route changed:", location.pathname);
  }, [location.pathname]);

  return null;
}

اطلاعات حساس کاربر یا محتوای خصوصی پیام را در ابزار Analytics و Logهای عمومی ثبت نکنید.

ارسال State هنگام Navigation

<Link
  to="/playground"
  state={{
    from: "/models",
    selectedModel: "YOUR_MODEL_ID",
  }}
>
  استفاده در Playground
</Link>

دریافت State:

import { useLocation } from "react-router";

export default function PlaygroundPage() {
  const location = useLocation();
  const selectedModel =
    location.state?.selectedModel;

  return (
    <p>
      مدل انتخاب‌شده:
      {selectedModel || "انتخاب نشده"}
    </p>
  );
}

Navigation State برای اطلاعات موقت مناسب است. برای داده‌ای که باید پس از Refresh باقی بماند یا قابل اشتراک‌گذاری باشد، URL، Backend یا State پایدار انتخاب مناسب‌تری است.

هرگز Secret را در Route State به‌عنوان راهکار امنیتی قرار ندهید.

Query String و useSearchParams

URL زیر را در نظر بگیرید:

/models?category=text&sort=popular&page=2

بخش بعد از ? Query String است.

برای خواندن آن:

import { useSearchParams } from "react-router";

export default function ModelsPage() {
  const [searchParams] = useSearchParams();

  const category =
    searchParams.get("category") || "all";

  const sort =
    searchParams.get("sort") || "popular";

  const page = Number(
    searchParams.get("page") || 1
  );

  return (
    <div>
      <p>دسته‌بندی: {category}</p>
      <p>مرتب‌سازی: {sort}</p>
      <p>صفحه: {page}</p>
    </div>
  );
}

برای تغییر Query Parameter:

import { useSearchParams } from "react-router";

export default function ModelFilters() {
  const [searchParams, setSearchParams] =
    useSearchParams();

  function changeCategory(category) {
    const nextParams =
      new URLSearchParams(searchParams);

    nextParams.set("category", category);
    nextParams.set("page", "1");

    setSearchParams(nextParams);
  }

  return (
    <button
      type="button"
      onClick={() => changeCategory("coding")}
    >
      مدل‌های برنامه‌نویسی
    </button>
  );
}

مقادیر Query String نیز ورودی کاربر هستند و باید اعتبارسنجی شوند.

چه داده‌ای را در URL قرار دهیم؟

URL برای Stateهایی مناسب است که:

  • قابل اشتراک‌گذاری‌اند.
  • باید Bookmark شوند.
  • بعد از Refresh باقی می‌مانند.
  • روی محتوای صفحه اثر می‌گذارند.
  • برای Back و Forward مرورگر مهم‌اند.

مثال:

/models?category=coding&sort=price

State محلی برای موارد موقت رابط کاربری مناسب‌تر است:

  • باز یا بسته بودن Modal
  • وضعیت Hover
  • متن موقت یک Input
  • Loading داخلی Component
  • انتخاب موقتی که نباید در URL دیده شود

اطلاعات حساس مانند API Key، Password و Token نباید در URL قرار بگیرند؛ زیرا URL ممکن است در History، Log، Analytics و Referrer ثبت شود.

ساخت داشبورد هوش مصنوعی چندصفحه‌ای

ساختار پروژه:

src/
├── components/
│   ├── Header.jsx
│   └── Sidebar.jsx
├── layouts/
│   ├── AppLayout.jsx
│   └── DashboardLayout.jsx
├── pages/
│   ├── HomePage.jsx
│   ├── PlaygroundPage.jsx
│   ├── ModelsPage.jsx
│   ├── HistoryPage.jsx
│   ├── ConversationPage.jsx
│   ├── SettingsPage.jsx
│   └── NotFoundPage.jsx
├── services/
│   └── chatService.js
├── App.jsx
├── main.jsx
└── index.css

ساخت AppLayout

فایل src/layouts/AppLayout.jsx:

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

export default function AppLayout() {
  return (
    <div className="app-shell">
      <header className="topbar">
        <Link
          className="brand"
          to="/"
        >
          داشبورد هوش مصنوعی
        </Link>

        <a
          href="https://darvareh.ir"
          target="_blank"
          rel="noreferrer"
        >
          درواره
        </a>
      </header>

      <Outlet />
    </div>
  );
}

ساخت DashboardLayout

فایل src/layouts/DashboardLayout.jsx:

import { NavLink, Outlet } from "react-router";

const navigationItems = [
  {
    to: "/dashboard",
    label: "نمای کلی",
    end: true,
  },
  {
    to: "/dashboard/playground",
    label: "Playground",
  },
  {
    to: "/dashboard/models",
    label: "مدل‌ها",
  },
  {
    to: "/dashboard/history",
    label: "تاریخچه",
  },
  {
    to: "/dashboard/settings",
    label: "تنظیمات",
  },
];

export default function DashboardLayout() {
  return (
    <div className="dashboard-layout">
      <aside className="sidebar">
        <nav>
          {navigationItems.map((item) => (
            <NavLink
              key={item.to}
              to={item.to}
              end={item.end}
              className={({ isActive }) =>
                isActive
                  ? "sidebar-link active"
                  : "sidebar-link"
              }
            >
              {item.label}
            </NavLink>
          ))}
        </nav>
      </aside>

      <main className="dashboard-content">
        <Outlet />
      </main>
    </div>
  );
}

تعریف Routeهای داشبورد

فایل src/App.jsx:

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

import AppLayout from "./layouts/AppLayout";
import DashboardLayout from "./layouts/DashboardLayout";

import HomePage from "./pages/HomePage";
import DashboardHomePage from "./pages/DashboardHomePage";
import PlaygroundPage from "./pages/PlaygroundPage";
import ModelsPage from "./pages/ModelsPage";
import HistoryPage from "./pages/HistoryPage";
import ConversationPage from "./pages/ConversationPage";
import SettingsPage from "./pages/SettingsPage";
import NotFoundPage from "./pages/NotFoundPage";

export default function App() {
  return (
    <Routes>
      <Route element={<AppLayout />}>
        <Route
          index
          element={<HomePage />}
        />

        <Route
          path="dashboard"
          element={<DashboardLayout />}
        >
          <Route
            index
            element={<DashboardHomePage />}
          />

          <Route
            path="playground"
            element={<PlaygroundPage />}
          />

          <Route
            path="models"
            element={<ModelsPage />}
          />

          <Route
            path="history"
            element={<HistoryPage />}
          />

          <Route
            path="conversations/:conversationId"
            element={<ConversationPage />}
          />

          <Route
            path="settings"
            element={<SettingsPage />}
          />
        </Route>

        <Route
          path="*"
          element={<NotFoundPage />}
        />
      </Route>
    </Routes>
  );
}

Routeهای نهایی:

/
/dashboard
/dashboard/playground
/dashboard/models
/dashboard/history
/dashboard/conversations/:conversationId
/dashboard/settings

ساخت صفحه Models با فیلتر URL

فایل src/pages/ModelsPage.jsx:

import { useSearchParams } from "react-router";

const categories = [
  {
    value: "all",
    label: "همه مدل‌ها",
  },
  {
    value: "text",
    label: "متنی",
  },
  {
    value: "coding",
    label: "برنامه‌نویسی",
  },
  {
    value: "image",
    label: "تصویری",
  },
];

export default function ModelsPage() {
  const [searchParams, setSearchParams] =
    useSearchParams();

  const selectedCategory =
    searchParams.get("category") || "all";

  function handleCategoryChange(event) {
    const nextCategory = event.target.value;

    if (nextCategory === "all") {
      setSearchParams({});
      return;
    }

    setSearchParams({
      category: nextCategory,
    });
  }

  return (
    <section>
      <div className="page-heading">
        <div>
          <p>مدیریت مدل‌ها</p>
          <h1>مدل‌های هوش مصنوعی</h1>
        </div>

        <a
          href="https://darvareh.ir/models"
          target="_blank"
          rel="noreferrer"
        >
          فهرست مدل‌ها و قیمت‌ها
        </a>
      </div>

      <label htmlFor="model-category">
        دسته‌بندی
      </label>

      <select
        id="model-category"
        value={selectedCategory}
        onChange={handleCategoryChange}
      >
        {categories.map((category) => (
          <option
            key={category.value}
            value={category.value}
          >
            {category.label}
          </option>
        ))}
      </select>

      <p>
        دسته انتخاب‌شده:
        {" "}
        {selectedCategory}
      </p>
    </section>
  );
}

با انتخاب دسته برنامه‌نویسی، URL تغییر می‌کند:

/dashboard/models?category=coding

اکنون این فیلتر قابل Bookmark و اشتراک‌گذاری است.

ساخت صفحه تاریخچه

فایل src/pages/HistoryPage.jsx:

import { Link } from "react-router";

const conversations = [
  {
    id: 101,
    title: "توضیح React Router",
  },
  {
    id: 102,
    title: "ساخت API با Node.js",
  },
  {
    id: 103,
    title: "بررسی کد JavaScript",
  },
];

export default function HistoryPage() {
  return (
    <section>
      <h1>تاریخچه مکالمات</h1>

      <ul className="conversation-list">
        {conversations.map((conversation) => (
          <li key={conversation.id}>
            <Link
              to={
                `/dashboard/conversations/${conversation.id}`
              }
            >
              {conversation.title}
            </Link>
          </li>
        ))}
      </ul>
    </section>
  );
}

ساخت صفحه جزئیات مکالمه

فایل src/pages/ConversationPage.jsx:

import {
  Link,
  useParams,
} from "react-router";

export default function ConversationPage() {
  const { conversationId } = useParams();

  const numericId = Number(conversationId);

  if (
    !Number.isInteger(numericId) ||
    numericId <= 0
  ) {
    return (
      <section>
        <h1>شناسه نامعتبر است</h1>

        <Link to="/dashboard/history">
          بازگشت به تاریخچه
        </Link>
      </section>
    );
  }

  return (
    <section>
      <p>جزئیات مکالمه</p>
      <h1>مکالمه شماره {numericId}</h1>

      <Link to="/dashboard/history">
        بازگشت به تاریخچه
      </Link>
    </section>
  );
}

در پروژه واقعی باید داده مکالمه از Backend دریافت شود. Backend باید بررسی کند مکالمه متعلق به کاربر فعلی است.

ساخت Playground هوش مصنوعی

فایل src/services/chatService.js:

export async function sendMessage(
  message,
  signal
) {
  const response = await fetch("/api/chat", {
    method: "POST",

    headers: {
      "Content-Type": "application/json",
    },

    body: JSON.stringify({
      message,
    }),

    signal,
  });

  const data =
    await response.json().catch(() => null);

  if (!response.ok) {
    throw new Error(
      data?.error ||
        "دریافت پاسخ با خطا روبه‌رو شد."
    );
  }

  if (typeof data?.answer !== "string") {
    throw new Error(
      "ساختار پاسخ قابل پردازش نیست."
    );
  }

  return data.answer;
}

فایل src/pages/PlaygroundPage.jsx:

import {
  useRef,
  useState,
} from "react";

import { sendMessage } from "../services/chatService";

export default function PlaygroundPage() {
  const [message, setMessage] = useState("");
  const [answer, setAnswer] = useState("");
  const [error, setError] = useState("");
  const [loading, setLoading] = useState(false);

  const controllerRef = useRef(null);

  async function handleSubmit(event) {
    event.preventDefault();

    const cleanMessage = message.trim();

    if (!cleanMessage || loading) {
      return;
    }

    controllerRef.current?.abort();
    controllerRef.current =
      new AbortController();

    setLoading(true);
    setError("");
    setAnswer("");

    try {
      const result = await sendMessage(
        cleanMessage,
        controllerRef.current.signal
      );

      setAnswer(result);
    } catch (requestError) {
      if (requestError.name !== "AbortError") {
        setError(
          requestError instanceof Error
            ? requestError.message
            : "خطای پیش‌بینی‌نشده‌ای رخ داد."
        );
      }
    } finally {
      setLoading(false);
      controllerRef.current = null;
    }
  }

  function handleCancel() {
    controllerRef.current?.abort();
  }

  return (
    <section>
      <p>محیط آزمایش</p>
      <h1>Playground هوش مصنوعی</h1>

      <form
        className="playground-form"
        onSubmit={handleSubmit}
      >
        <label htmlFor="message">
          پیام
        </label>

        <textarea
          id="message"
          value={message}
          onChange={(event) =>
            setMessage(event.target.value)
          }
          rows="8"
          disabled={loading}
          placeholder="پیام خود را وارد کنید."
        />

        <div className="form-actions">
          {loading && (
            <button
              type="button"
              onClick={handleCancel}
            >
              توقف
            </button>
          )}

          <button
            type="submit"
            disabled={
              loading || !message.trim()
            }
          >
            {loading
              ? "در حال دریافت پاسخ..."
              : "ارسال"}
          </button>
        </div>
      </form>

      {error && (
        <p role="alert">
          {error}
        </p>
      )}

      {answer && (
        <article>
          <h2>پاسخ</h2>
          <p>{answer}</p>
        </article>
      )}
    </section>
  );
}

چرا API Key نباید در React قرار بگیرد؟

کد React به مرورگر کاربر ارسال می‌شود. قرار دادن API Key در Source Code، فایل JavaScript یا متغیر محیطی قابل‌دسترسی Frontend باعث می‌شود کاربر بتواند آن را مشاهده کند.

این روش ناامن است:

fetch(
  "https://api.darvareh.ir/v1/chat/completions",
  {
    headers: {
      Authorization:
        "Bearer YOUR_DARVAREH_API_KEY",
    },
  }
);

این روش نیز امن نیست:

VITE_DARVAREH_API_KEY=YOUR_DARVAREH_API_KEY

معماری مناسب:

React Frontend
      |
      | /api/chat
      v
Backend برنامه شما
      |
      | API Key خصوصی
      v
API هوش مصنوعی درواره

ساخت Backend امن

در ریشه پروژه:

mkdir server
cd server
npm init -y
npm install express dotenv

فایل server/package.json:

{
  "name": "react-router-ai-backend",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "node --watch server.js",
    "start": "node server.js"
  }
}

فایل server/.env:

DARVAREH_API_KEY=YOUR_DARVAREH_API_KEY
DARVAREH_MODEL_ID=YOUR_MODEL_ID
PORT=3000

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

فایل .env را وارد Git نکنید:

server/.env
node_modules
dist

پیاده‌سازی Endpoint

فایل server/server.js:

import "dotenv/config";
import express from "express";

const app = express();
const port = Number(
  process.env.PORT || 3000
);

app.disable("x-powered-by");

app.use(
  express.json({
    limit: "32kb",
  })
);

app.get(
  "/api/health",
  (request, response) => {
    response.json({
      status: "ok",
    });
  }
);

app.post(
  "/api/chat",
  async (request, response) => {
    const message =
      typeof request.body?.message === "string"
        ? request.body.message.trim()
        : "";

    if (!message) {
      return response.status(400).json({
        error: "متن پیام الزامی است.",
      });
    }

    if (message.length > 4000) {
      return response.status(400).json({
        error:
          "طول پیام بیشتر از مقدار مجاز است.",
      });
    }

    if (
      !process.env.DARVAREH_API_KEY ||
      !process.env.DARVAREH_MODEL_ID
    ) {
      console.error(
        "Darvareh configuration is missing."
      );

      return response.status(500).json({
        error:
          "تنظیمات سرویس کامل نیست.",
      });
    }

    try {
      const upstreamResponse = await fetch(
        "https://api.darvareh.ir/v1/chat/completions",
        {
          method: "POST",

          headers: {
            Authorization:
              `Bearer ${process.env.DARVAREH_API_KEY}`,
            "Content-Type":
              "application/json",
          },

          body: JSON.stringify({
            model:
              process.env.DARVAREH_MODEL_ID,

            messages: [
              {
                role: "system",
                content:
                  "شما یک دستیار فارسی دقیق و کاربردی هستید.",
              },
              {
                role: "user",
                content: message,
              },
            ],

            temperature: 0.4,
          }),
        }
      );

      const data =
        await upstreamResponse
          .json()
          .catch(() => null);

      if (!upstreamResponse.ok) {
        console.error(
          "Darvareh API error:",
          {
            status:
              upstreamResponse.status,
          }
        );

        return response.status(502).json({
          error:
            "سرویس هوش مصنوعی پاسخ معتبری برنگرداند.",
        });
      }

      const answer =
        data?.choices?.[0]?.message?.content;

      if (
        typeof answer !== "string" ||
        !answer.trim()
      ) {
        return response.status(502).json({
          error:
            "ساختار پاسخ قابل پردازش نبود.",
        });
      }

      return response.json({
        answer: answer.trim(),
      });
    } catch (error) {
      console.error(
        "Unexpected error:",
        error
      );

      return response.status(500).json({
        error:
          "ارتباط با سرویس برقرار نشد.",
      });
    }
  }
);

app.listen(port, () => {
  console.log(
    `Backend is running on port ${port}`
  );
});

تنظیم Proxy در Vite

فایل vite.config.js:

import {
  defineConfig,
} from "vite";

import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [
    react(),
  ],

  server: {
    proxy: {
      "/api": {
        target:
          "http://localhost:3000",
        changeOrigin: true,
      },
    },
  },
});

اکنون درخواست /api/chat در محیط توسعه به Backend روی پورت 3000 هدایت می‌شود.

Proxy مربوط به Development Server است و در Build نهایی فعال نیست. برای Production باید مسیر Backend را در Web Server یا سرویس Hosting تنظیم کنید.

Protected Route چیست؟

Protected Route رابط کاربری یک Route را فقط در صورت وجود وضعیت احراز هویت نمایش می‌دهد.

مثال ساده:

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

export default function ProtectedRoute({
  authenticated,
}) {
  const location = useLocation();

  if (!authenticated) {
    return (
      <Navigate
        to="/login"
        replace
        state={{
          from: location.pathname,
        }}
      />
    );
  }

  return <Outlet />;
}

استفاده:

<Routes>
  <Route
    path="/login"
    element={<LoginPage />}
  />

  <Route
    element={
      <ProtectedRoute
        authenticated={authenticated}
      />
    }
  >
    <Route
      path="/dashboard"
      element={<DashboardLayout />}
    >
      <Route
        index
        element={<DashboardHomePage />}
      />
    </Route>
  </Route>
</Routes>

پس از Login:

const location = useLocation();
const navigate = useNavigate();

const destination =
  location.state?.from || "/dashboard";

navigate(destination, {
  replace: true,
});

Protected Route مرز امنیتی نیست

کاربر می‌تواند JavaScript Frontend را تغییر دهد، Request دستی ارسال کند یا مستقیم Endpoint را فراخوانی کند.

بنابراین Backend باید برای هر Request بررسی کند:

  • کاربر احراز هویت شده است؟
  • حساب فعال است؟
  • کاربر مالک Resource است؟
  • نقش مناسب دارد؟
  • اجازه استفاده از مدل انتخابی را دارد؟
  • محدودیت مصرف رعایت شده است؟

Protected Route فقط تجربه کاربری و Navigation را کنترل می‌کند.

Role-based Route

برای نمایش Route براساس نقش:

function RoleRoute({
  user,
  allowedRoles,
}) {
  if (!user) {
    return (
      <Navigate
        to="/login"
        replace
      />
    );
  }

  if (
    !allowedRoles.includes(user.role)
  ) {
    return (
      <Navigate
        to="/forbidden"
        replace
      />
    );
  }

  return <Outlet />;
}

استفاده:

<Route
  element={
    <RoleRoute
      user={user}
      allowedRoles={["admin"]}
    />
  }
>
  <Route
    path="/admin"
    element={<AdminPage />}
  />
</Route>

Backend همچنان باید Role را مستقل بررسی کند.

Lazy Loading صفحه‌ها

اگر تمام صفحات در Bundle اولیه قرار بگیرند، حجم JavaScript اولیه افزایش پیدا می‌کند. می‌توان صفحه‌های سنگین را با lazy بارگذاری کرد.

import {
  lazy,
  Suspense,
} from "react";

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

const PlaygroundPage = lazy(
  () =>
    import(
      "./pages/PlaygroundPage"
    )
);

const ModelsPage = lazy(
  () =>
    import("./pages/ModelsPage")
);

export default function App() {
  return (
    <Suspense
      fallback={
        <p>در حال بارگذاری صفحه...</p>
      }
    >
      <Routes>
        <Route
          path="/playground"
          element={<PlaygroundPage />}
        />

        <Route
          path="/models"
          element={<ModelsPage />}
        />
      </Routes>
    </Suspense>
  );
}

Lazy Loading برای صفحه‌هایی مناسب است که:

  • در ورود اولیه لازم نیستند.
  • کتابخانه‌های سنگین دارند.
  • تعداد کاربران کمتری به آن‌ها مراجعه می‌کنند.
  • شامل Chart یا Editor بزرگ هستند.

هر Component کوچک را بدون اندازه‌گیری Lazy نکنید. تعداد زیاد Chunk نیز می‌تواند مدیریت و بارگذاری را پیچیده کند.

ساخت Error Boundary در Data Router

در Data Mode می‌توان Error Element مربوط به Route تعریف کرد.

نمونه Router:

import {
  createBrowserRouter,
  RouterProvider,
} from "react-router";

import AppLayout from "./layouts/AppLayout";
import HomePage from "./pages/HomePage";
import ModelsPage from "./pages/ModelsPage";
import RouteErrorPage from "./pages/RouteErrorPage";

const router = createBrowserRouter([
  {
    path: "/",
    element: <AppLayout />,
    errorElement: <RouteErrorPage />,

    children: [
      {
        index: true,
        element: <HomePage />,
      },
      {
        path: "models",
        element: <ModelsPage />,
      },
    ],
  },
]);

export default function App() {
  return (
    <RouterProvider
      router={router}
    />
  );
}

در این ساختار دیگر نباید BrowserRouter جداگانه دور RouterProvider قرار دهید.

Loader در Data Router

Loader می‌تواند پیش از نمایش Route داده موردنیاز را دریافت کند:

import {
  createBrowserRouter,
  RouterProvider,
} from "react-router";

async function modelsLoader() {
  const response = await fetch(
    "/api/models"
  );

  if (!response.ok) {
    throw new Response(
      "Models could not be loaded",
      {
        status: response.status,
      }
    );
  }

  return response.json();
}

تعریف Route:

const router = createBrowserRouter([
  {
    path: "/models",
    loader: modelsLoader,
    element: <ModelsPage />,
  },
]);

استفاده از داده:

import {
  useLoaderData,
} from "react-router";

export default function ModelsPage() {
  const models = useLoaderData();

  return (
    <ul>
      {models.map((model) => (
        <li key={model.id}>
          {model.name}
        </li>
      ))}
    </ul>
  );
}

Data Router امکانات گسترده‌تری دارد، اما برای پروژه کوچک ممکن است Declarative Mode کافی باشد.

BrowserRouter یا HashRouter؟

BrowserRouter

URL تمیز تولید می‌کند:

https://example.com/dashboard/models

اما Web Server باید تمام Routeهای Frontend را به index.html Rewrite کند.

HashRouter

Route بعد از # قرار می‌گیرد:

https://example.com/#/dashboard/models

بخش Hash معمولاً به سرور ارسال نمی‌شود؛ بنابراین در Hostingهای بسیار ساده احتمال خطای Refresh کمتر است.

مثال:

import {
  HashRouter,
} from "react-router";

<HashRouter>
  <App />
</HashRouter>

برای بیشتر پروژه‌های حرفه‌ای با کنترل مناسب روی Hosting، BrowserRouter و تنظیم Rewrite انتخاب رایج‌تری است. HashRouter می‌تواند برای Static Hosting محدود یا ابزارهای خاص کاربرد داشته باشد.

علت خطای 404 پس از Refresh

فرض کنید در برنامه به این Route رفته‌اید:

/dashboard/models

Navigation داخلی درست کار می‌کند؛ اما با Refresh، مرورگر مستقیماً از سرور همین Path را درخواست می‌کند:

GET /dashboard/models

اگر سرور فقط فایل /index.html را بشناسد، خطای 404 می‌دهد.

راه‌حل این است که سرور برای Routeهای Frontend، index.html را برگرداند تا React Router URL را پردازش کند.

تنظیم Nginx برای React Router

نمونه ساده:

server {
    listen 80;
    server_name example.com;

    root /var/www/app/dist;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    location /api/ {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

در این تنظیم:

  • فایل موجود مستقیماً ارائه می‌شود.
  • اگر فایل یا پوشه وجود نداشته باشد، index.html برگردانده می‌شود.
  • درخواست‌های /api/ به Backend هدایت می‌شوند.

تنظیم Production باید براساس زیرساخت واقعی، HTTPS، Cache و نیازهای Backend بازبینی شود.

ارائه Build React با Express

بعد از Build:

npm run build

می‌توانید فایل‌های dist را با Express ارائه کنید:

import path from "node:path";

import {
  fileURLToPath,
} from "node:url";

const currentFile =
  fileURLToPath(import.meta.url);

const currentDirectory =
  path.dirname(currentFile);

const frontendDirectory =
  path.resolve(
    currentDirectory,
    "../dist"
  );

app.use(
  express.static(
    frontendDirectory
  )
);

بعد از تعریف Routeهای /api و قبل از app.listen، Fallback مربوط به SPA را اضافه کنید. Syntax دقیق Catch-all می‌تواند با نسخه Express تفاوت داشته باشد؛ بنابراین آن را با نسخه نصب‌شده بررسی کنید.

هدف Fallback این است که Routeهایی مانند زیر index.html را دریافت کنند:

/dashboard
/dashboard/models
/dashboard/conversations/42

تنظیم basename

اگر برنامه در زیرمسیر منتشر می‌شود:

https://example.com/ai-dashboard/

می‌توانید basename تعریف کنید:

<BrowserRouter
  basename="/ai-dashboard"
>
  <App />
</BrowserRouter>

حالا Route داخلی /models در این آدرس نمایش داده می‌شود:

https://example.com/ai-dashboard/models

تنظیم base در Vite و basename در Router باید با مسیر استقرار هماهنگ باشند.

بازگرداندن Scroll به بالای صفحه

در SPA، Navigation همیشه Scroll را مانند بارگذاری سنتی صفحه Reset نمی‌کند. یک Component ساده بسازید:

import {
  useEffect,
} from "react";

import {
  useLocation,
} from "react-router";

export default function ScrollToTop() {
  const { pathname } =
    useLocation();

  useEffect(() => {
    window.scrollTo({
      top: 0,
      left: 0,
      behavior: "instant",
    });
  }, [pathname]);

  return null;
}

استفاده:

<BrowserRouter>
  <ScrollToTop />
  <App />
</BrowserRouter>

برای سناریوهای پیچیده‌تر مانند حفظ Scroll در صفحات فهرستی باید رفتار دقیق‌تری طراحی کنید.

تنظیم عنوان صفحه براساس Route

برای هر Page می‌توانید عنوان Document را تغییر دهید:

import {
  useEffect,
} from "react";

export default function ModelsPage() {
  useEffect(() => {
    document.title =
      "مدل‌های هوش مصنوعی | داشبورد";
  }, []);

  return <h1>مدل‌ها</h1>;
}

یک Hook قابل استفاده مجدد:

import {
  useEffect,
} from "react";

export function usePageTitle(title) {
  useEffect(() => {
    const previousTitle =
      document.title;

    document.title = title;

    return () => {
      document.title =
        previousTitle;
    };
  }, [title]);
}

استفاده:

usePageTitle(
  "Playground هوش مصنوعی"
);

برای سایت‌های محتوایی و SEOمحور، فقط تغییر Client-side عنوان کافی نیست و باید Rendering، HTML اولیه، Meta Tagها و دسترسی خزنده‌ها نیز بررسی شود.

Route و SEO

React Router ابزار Routing است، نه یک راهکار کامل SEO.

برای داشبوردهای خصوصی و ابزارهای تعاملی، Client-side Routing معمولاً مناسب است. اما برای صفحات عمومی وابسته به ترافیک ارگانیک باید موارد زیر بررسی شوند:

  • HTML اولیه
  • Title و Meta Description
  • Canonical URL
  • Structured Data
  • Server-side Rendering
  • Static Generation
  • Status Code صحیح
  • Sitemap
  • لینک داخلی واقعی
  • سرعت بارگذاری
  • محتوای قابل مشاهده برای خزنده

صرف داشتن URLهای جداگانه تضمین نمی‌کند همه صفحات به‌درستی ایندکس شوند.

تست Routeها با MemoryRouter

برای تست Component وابسته به Router می‌توان از MemoryRouter استفاده کرد:

import {
  MemoryRouter,
} from "react-router";

render(
  <MemoryRouter
    initialEntries={[
      "/dashboard/models?category=coding",
    ]}
  >
    <ModelsPage />
  </MemoryRouter>
);

MemoryRouter History را در حافظه نگه می‌دارد و برای تست یا محیط‌های غیرمرورگری مناسب است.

نمونه تست ساده:

import {
  render,
  screen,
} from "@testing-library/react";

import {
  MemoryRouter,
} from "react-router";

test(
  "models page reads category from URL",
  () => {
    render(
      <MemoryRouter
        initialEntries={[
          "/models?category=coding",
        ]}
      >
        <ModelsPage />
      </MemoryRouter>
    );

    expect(
      screen.getByText(
        /coding/i
      )
    ).toBeInTheDocument();
  }
);

تست Navigation

function TestRoutes() {
  return (
    <Routes>
      <Route
        path="/"
        element={
          <Link to="/models">
            مدل‌ها
          </Link>
        }
      />

      <Route
        path="/models"
        element={
          <h1>مدل‌های هوش مصنوعی</h1>
        }
      />
    </Routes>
  );
}

تست:

render(
  <MemoryRouter
    initialEntries={["/"]}
  >
    <TestRoutes />
  </MemoryRouter>
);

await user.click(
  screen.getByRole(
    "link",
    {
      name: "مدل‌ها",
    }
  )
);

expect(
  screen.getByRole(
    "heading",
    {
      name:
        "مدل‌های هوش مصنوعی",
    }
  )
).toBeInTheDocument();

اشتباهات رایج React Router

استفاده از Router تو‌در‌تو

این ساختار معمولاً اشتباه است:

<BrowserRouter>
  <App>
    <BrowserRouter>
      <Dashboard />
    </BrowserRouter>
  </App>
</BrowserRouter>

در بیشتر برنامه‌ها یک Router در ریشه کافی است.

فراموش کردن Outlet

Route فرزند تعریف شده، اما نمایش داده نمی‌شود:

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

والد باید Outlet داشته باشد:

function DashboardLayout() {
  return (
    <main>
      <h1>داشبورد</h1>
      <Outlet />
    </main>
  );
}

استفاده از href برای لینک داخلی

به‌جای:

<a href="/settings">
  تنظیمات
</a>

استفاده کنید:

<Link to="/settings">
  تنظیمات
</Link>

استفاده از مسیر مطلق اشتباه در Route فرزند

در Nested Routes معمولاً Path فرزند نسبی نوشته می‌شود:

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

Route نهایی:

/dashboard/settings

استفاده از useNavigate خارج Router

Hookهای React Router فقط داخل Context آن قابل‌استفاده‌اند.

اعتماد به Protected Route برای امنیت

Backend باید Access Control را مستقل اجرا کند.

قرار دادن Secret در URL

این کار خطرناک است:

/playground?apiKey=secret

Secret ممکن است در History، Log و Analytics ثبت شود.

نداشتن Route مربوط به 404

<Route
  path="*"
  element={<NotFoundPage />}
/>

نداشتن Rewrite روی سرور

Navigation داخلی ممکن است کار کند، اما Refresh یک Route با 404 مواجه شود.

ذخیره تمام State در URL

URL برای State قابل اشتراک‌گذاری مناسب است، نه تمام جزئیات موقت UI.

استفاده بی‌دلیل از useNavigate

برای لینک قابل‌کلیک از Link یا NavLink استفاده کنید.

خطاهای رایج و راه‌حل آن‌ها

useNavigate may be used only in the context of a Router

Component خارج BrowserRouter است.

در main.jsx:

<BrowserRouter>
  <App />
</BrowserRouter>

No routes matched location

هیچ Route مطابق با URL وجود ندارد. Pathها و Route مربوط به * را بررسی کنید:

<Route
  path="*"
  element={<NotFoundPage />}
/>

صفحه Nested خالی است

وجود Outlet در Layout والد را بررسی کنید.

لینک فعال اشتباه است

برای Route ریشه از end استفاده کنید:

<NavLink
  to="/"
  end
>
  خانه
</NavLink>

Refresh در Production خطای 404 می‌دهد

Web Server باید Fallback به index.html داشته باشد.

Query Parameter به‌روزرسانی نمی‌شود

به‌جای تغییر مستقیم Object موجود، یک URLSearchParams جدید بسازید:

const nextParams =
  new URLSearchParams(
    searchParams
  );

nextParams.set(
  "category",
  "coding"
);

setSearchParams(nextParams);

Route Parameter مقدار undefined دارد

نام Parameter در Route و useParams باید یکسان باشد:

<Route
  path="/models/:modelId"
  element={<ModelPage />}
/>
const { modelId } =
  useParams();

Importها با آموزش اینترنتی متفاوت‌اند

نسخه Package را بررسی کنید:

npm list react-router react-router-dom

سپس مستندات همان نسخه را بخوانید. آموزش‌های قدیمی ممکن است از API و Importهای نسخه متفاوت استفاده کنند.

بهترین روش طراحی Routeها

Routeها باید ساختار محصول را نشان دهند:

/dashboard
/dashboard/models
/dashboard/playground
/dashboard/history
/dashboard/settings

بهتر است:

  • URLها کوتاه و قابل‌فهم باشند.
  • نام‌ها پایدار باشند.
  • ساختار بیش از حد عمیق نشود.
  • IDهای حساس یا Secret وارد URL نشوند.
  • Routeهای مرتبط Layout مشترک داشته باشند.
  • صفحه 404 طراحی شود.
  • Redirect مسیرهای قدیمی مشخص باشد.
  • دسترسی Backend مستقل بررسی شود.

سازمان‌دهی Routeها در پروژه بزرگ

در پروژه کوچک می‌توان Routeها را در App.jsx نگه داشت. در پروژه بزرگ بهتر است فایل جداگانه داشته باشید:

src/
├── router/
│   ├── AppRoutes.jsx
│   ├── ProtectedRoute.jsx
│   └── RoleRoute.jsx

فایل AppRoutes.jsx:

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

export default function AppRoutes() {
  return (
    <Routes>
      {/* Route definitions */}
    </Routes>
  );
}

در App.jsx:

import AppRoutes from "./router/AppRoutes";

export default function App() {
  return <AppRoutes />;
}

از تقسیم بیش از حد Routeها در پروژه کوچک خودداری کنید. ساختار باید پیچیدگی واقعی محصول را منعکس کند.

چک‌لیست انتشار پروژه React Router

پیش از انتشار بررسی کنید:

  • Router فقط یک‌بار در ریشه برنامه قرار گرفته است.
  • همه Routeهای اصلی تعریف شده‌اند.
  • Nested Routeها Outlet دارند.
  • صفحه 404 وجود دارد.
  • Navigation داخلی با Link یا NavLink انجام می‌شود.
  • لینک خارجی از Anchor مناسب استفاده می‌کند.
  • Dynamic Parameterها اعتبارسنجی می‌شوند.
  • Query Parameterها اعتبارسنجی می‌شوند.
  • Secret داخل URL قرار نمی‌گیرد.
  • Protected Route فقط برای UX در نظر گرفته شده است.
  • Backend دسترسی واقعی را بررسی می‌کند.
  • API Key فقط در Backend نگهداری می‌شود.
  • Routeهای سنگین در صورت نیاز Lazy Load می‌شوند.
  • Loading و Error State طراحی شده است.
  • Refresh مستقیم Routeها آزمایش شده است.
  • Rewrite سرور تنظیم شده است.
  • مقدار basename با مسیر انتشار سازگار است.
  • Title صفحه‌ها تنظیم شده است.
  • Navigation با Keyboard قابل‌استفاده است.
  • Routeها در موبایل آزمایش شده‌اند.
  • تست Routeهای مهم نوشته شده است.
  • Lint، Test و Build بدون خطا اجرا می‌شوند.

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

React Router چیست؟

React Router کتابخانه‌ای برای هماهنگ کردن URL مرورگر با Componentهای برنامه React است. با آن می‌توان صفحه‌ها، مسیرهای تو‌در‌تو، مسیرهای پویا و Navigation سمت Client ساخت.

آیا React Router بخشی از React است؟

خیر. React Router یک کتابخانه جداگانه است و باید به پروژه اضافه شود.

react-router-dom چیست؟

react-router-dom Package شناخته‌شده نسخه‌های قبلی و بسیاری از پروژه‌های موجود برای Routing در مرورگر است. در مستندات نسخه فعلی، Declarative Mode با Package react-router آموزش داده می‌شود. همیشه نسخه پروژه خود را بررسی کنید.

چگونه React Router را نصب کنیم؟

براساس مستندات فعلی:

npm install react-router

برای پروژه موجود ابتدا نسخه و Package فعلی را بررسی کنید.

BrowserRouter چیست؟

BrowserRouter برنامه را به History API مرورگر متصل می‌کند و URLهای معمولی مانند /dashboard/models می‌سازد.

Route چیست؟

Route یک Path را به Element رابط کاربری متصل می‌کند:

<Route
  path="/models"
  element={<ModelsPage />}
/>

Outlet چیست؟

Outlet محل نمایش Route فرزند در Layout والد است.

useParams چه کاری انجام می‌دهد؟

Dynamic Parameterهای URL را برمی‌گرداند:

const { conversationId } =
  useParams();

useNavigate چه کاربردی دارد؟

برای Navigation برنامه‌نویسی‌شده پس از رویدادهایی مانند Login، ثبت فرم یا ساخت Resource استفاده می‌شود.

useSearchParams چیست؟

برای خواندن و تغییر Query String URL استفاده می‌شود:

const [
  searchParams,
  setSearchParams,
] = useSearchParams();

چرا پس از Refresh خطای 404 می‌گیرم؟

زیرا سرور Route Frontend را نمی‌شناسد. باید Web Server طوری تنظیم شود که Routeهای SPA را به index.html Rewrite کند.

Protected Route برای امنیت کافی است؟

خیر. Protected Route فقط نمایش رابط کاربری را کنترل می‌کند. Backend باید احراز هویت و مجوز دسترسی را برای هر Request بررسی کند.

آیا React Router برای SEO مناسب است؟

React Router فقط Routing را فراهم می‌کند. SEO به Rendering، HTML اولیه، Meta Tagها، Status Code، Sitemap، محتوای صفحه و دسترسی خزنده‌ها نیز وابسته است.

چگونه صفحه 404 بسازیم؟

<Route
  path="*"
  element={<NotFoundPage />}
/>

چگونه API هوش مصنوعی را به Route Playground متصل کنیم؟

Frontend باید Request را به Backend شما بفرستد و Backend با API Key خصوصی به API هوش مصنوعی متصل شود:

React -> Backend -> API درواره

شناسه مدل و قیمت را از کجا ببینیم؟

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

جمع‌بندی

React Router ابزار اصلی ساخت Routing در بسیاری از اپلیکیشن‌های React است. این کتابخانه URL را به Componentهای رابط کاربری متصل می‌کند و امکاناتی مانند Navigation بدون Reload کامل، Routeهای تو‌در‌تو، Layout مشترک، Dynamic Parameter و Query String را فراهم می‌کند.

مهم‌ترین نکات این آموزش:

  • BrowserRouter برنامه را به History مرورگر متصل می‌کند.
  • Routes و Route مسیرها را تعریف می‌کنند.
  • برای Navigation داخلی از Link و NavLink استفاده می‌شود.
  • Outlet محل نمایش Route فرزند است.
  • useParams Dynamic Parameterها را می‌خواند.
  • useSearchParams Query String را مدیریت می‌کند.
  • useNavigate برای Navigation پس از عملیات مناسب است.
  • Protected Route یک مرز امنیتی واقعی نیست.
  • Backend باید Access Control را مستقل اجرا کند.
  • API Key هوش مصنوعی نباید در React قرار بگیرد.
  • برای Refresh مستقیم Routeها باید Rewrite سرور تنظیم شود.
  • صفحه‌های سنگین را می‌توان به‌صورت Lazy بارگذاری کرد.
  • نسخه React Router باید با مستندات مورداستفاده تطبیق داده شود.

با استفاده از React Router می‌توانید یک داشبورد چندصفحه‌ای برای Playground، مدل‌ها، تاریخچه درخواست‌ها و تنظیمات بسازید و Backend آن را به API هوش مصنوعی درواره متصل کنید.

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

مقالات مرتبط

منابع تکمیلی

برای مطالعه شرایط استفاده و محدودیت‌های مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.

Read more

اتوماسیون هوش مصنوعی چیست؟ کاربردها و آموزش ساخت AI Automation

اتوماسیون هوش مصنوعی چیست؟ کاربردها و آموزش ساخت AI Automation

اتوماسیون هوش مصنوعی با ترکیب گردش‌کارهای خودکار و مدل‌های هوش مصنوعی، پردازش متن، دسته‌بندی، استخراج اطلاعات و تصمیم‌های پیشنهادی را خودکار می‌کند. در این راهنما، معماری و ساخت نمونه عملی آن با API درواره را می‌آموزید.

Agentic Commerce چیست؟ آینده خرید با ایجنت هوش مصنوعی

Agentic Commerce چیست؟ آینده خرید با ایجنت هوش مصنوعی

Agentic Commerce شیوه‌ای جدید برای خرید اینترنتی است که در آن ایجنت هوش مصنوعی می‌تواند نیاز کاربر را بفهمد، محصولات را جست‌وجو و مقایسه کند و فرایند خرید را پیش ببرد. در این راهنما با معماری، UCP، ACP و پیاده‌سازی آن با API درواره آشنا می‌شوید.