۶ React Router یک کتابخانه routing محبوب و قدرتمند برای برنامه‌های React است. کتابخانه React Router یک رویکرد declarative و مبتنی بر کامپوننت برای مسیریابی ارائه می‌دهد و وظایف رایج مربوط به رسیدگی به پارامترهای URL، redirectها و بارگذاری داده‌ها را انجام می‌دهد.

React Router یکی از بصری‌ترین APIهای موجود را ارائه می‌دهد و lazy loading و رندر سمت سرور سازگار با SEO را امکان‌پذیر می‌کند.

در این قاله قصد داریم تا نحوه ایجاد مسیرهای محافظت شده و احراز هویت با نسخه ۶ React Router را باهم بررسی کنیم.

شروع کار با React Router

برای شروع، ترمینال را باز می‌کنیم، با اجرای دستور زیر و با استفاده از vite یک پروژه React جدید می‌سازیم:

npm create vite@latest ReactRouterAuthDemo -- --template react
cd ReactRouterAuthDemo

سپس، React Router را به عنوان یک dependency در برنامه React نصب می‌کنیم:

npm install react-router-dom

هنگامی که React Router نصب شد، باید فایل src/main.js را ویرایش کنیم.

BrowserRouter را از react-router-dom import می‌کنیم و سپس کامپوننت <App /> را داخل <BrowserRouter /> قرار می‌دهیم، به این صورت که:

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./index.css";

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

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

اکنون، تنظیماتی که انجام دادیم به گونه‌ای است که می‌توانیم از کامپوننت‌های React Router و Hooks در هر نقطه از برنامه خود استفاده کنیم.

حال قصد داریم تا boilerplate code از فایل App.js را با برخی از مسیرها جایگزین نماییم.

مسیریابی بیسیک با استفاده از React Router

React Router کامپوننت‌های <Routes /> و <Route /> را فراهم می‌کند که ما را قادر می‌سازد تا کامپوننت‌ها را بر اساس مکان فعلی آن‌ها رندر کنیم:

// src/App.jsx

import { Routes, Route } from "react-router-dom";
import { LoginPage } from "./pages/Login";
import { HomePage } from "./pages/Home";

import "./App.css";

function App() {
  return (
    <Routes>
      <Route path="/" element={<HomePage />} />
      <Route path="/login" element={<LoginPage />} />
    </Routes>
  );
}

export default App;

مسیریابی بیسیک با استفاده از <Route >

<Route /> mapping بین pathهای موجود در برنامه و کامپوننت‌های مختلف React را ارائه می‌دهد. به عنوان مثال، هنگامی که شخصی به مسیر /login هدایت می‌شود، برای این که کامپوننت LoginPage را رندر کنیم فقط باید <Route /> را ارائه دهیم، مانند:

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

کامپوننت <Route /> را می‌توانیم مانند یک عبارت if در نظر بگیریم. به این ترتیب، فقط در صورتی که با path مشخص شده مطابقت داشته باشد، بر روی یک لوکیشین URL با element خود عمل می‌کند.

مسیریابی بیسیک با استفاده از <Routes />

کامپوننت <Routes /> جایگزینی برای کامپوننت <Switch /> از نسخه ۵ React Router است. برای استفاده از <Routes />، ابتدا فایل‌های Login.jsx و Home.jsx را در دایرکتوری pages با محتوای زیر ایجاد می‌کنیم:

// Login.jsx
export const LoginPage = () => (
  <div>
    <h1>This is the Login Page</h1>
  </div>
);

// Home.jsx
export const HomePage = () => (
  <div>
    <h1>This is the Home Page</h1>
  </div>
);

در مرحله بعد، دستور زیر را برای شروع برنامه اجرا می‌نماییم:

npm run dev

در مرورگر، به طور پیش‌فرض کامپوننت HomePage را می‌بینیم. اگر به مسیر /login برویم، کامپوننت LoginPage را در صفحه نمایش مشاهده خواهیم کرد. از طرف دیگر، می‌توانیم از یک آبجکت جاوااسکریپت ساده برای نشان دادن مسیرها در برنامه خود با استفاده از هوک useRoutes استفاده نماییم. این یک رویکرد فانکشنال برای تعریف مسیرها است و مانند ترکیب کامپوننت‌های <Routes /> و <Route /> کار می‌کند:

// src/App.jsx

import { useRoutes } from "react-router-dom";
// ...

export default function App() {
  const routes = useRoutes([
    {
      path: "/",
      element: <HomePage />
    },
    {
      path: "/login",
      element: <LoginPage />
    }
  ]);
  return routes;
}

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

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

مسیرهای محافظت شده، که اغلب به عنوان مسیرهای private شناخته می‌شوند، یک مفهوم اساسی در توسعه وب است که برای محدود کردن دسترسی به صفحات یا منابع خاص فقط برای کاربران تأیید شده مورد استفاده قرار می‌گیرد.

برای پیاده‌سازی یک مسیر محافظت‌شده در پروژه، کار خود را با ایجاد یک هوک سفارشی با نام useAuth برای مدیریت state کاربر احراز هویت شده با استفاده از Context API و هوک useContext شروع می‌کنیم:

// src/hooks/useAuth.jsx

import { createContext, useContext, useMemo } from "react";
import { useNavigate } from "react-router-dom";
import { useLocalStorage } from "./useLocalStorage";
const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useLocalStorage("user", null);
  const navigate = useNavigate();

  // call this function when you want to authenticate the user
  const login = async (data) => {
    setUser(data);
    navigate("/profile");
  };

  // call this function to sign out logged in user
  const logout = () => {
    setUser(null);
    navigate("/", { replace: true });
  };

  const value = useMemo(
    () => ({
      user,
      login,
      logout,
    }),
    [user]
  );
  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};

export const useAuth = () => {
  return useContext(AuthContext);
};

این هوک useAuth state و متدهای user را برای ورود و خروج کاربران نشان می‌دهد. هنگامی که کاربران با موفقیت وارد سیستم می‌شوند، متد login() state آن‌ها را تغییر می‌دهد تا وضعیت احراز هویت آن‌ها را منعکس کند. علاوه بر این، هنگامی که کاربران از سیستم خارج می‌شوند، با استفاده از هوک useNavigate در React Router، آن‌ها را به صفحه اصلی هدایت می‌کنیم.

برای این که بتوانیم state کاربر را حتی پس از به‌روزرسانی صفحه حفظ نماییم، هوک useLocalStorage را ایجاد می‌کنیم که مقدار state را با local storage مرورگر همگام‌سازی می‌نماید:

// src/hooks/useLocalStorage.jsx

import { useState } from "react";

export const useLocalStorage = (keyName, defaultValue) => {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const value = window.localStorage.getItem(keyName);
      if (value) {
        return JSON.parse(value);
      } else {
        window.localStorage.setItem(keyName, JSON.stringify(defaultValue));
        return defaultValue;
      }
    } catch (err) {
      return defaultValue;
    }
  });
  const setValue = (newValue) => {
    try {
      window.localStorage.setItem(keyName, JSON.stringify(newValue));
    } catch (err) {
      console.log(err);
    }
    setStoredValue(newValue);
  };
  return [storedValue, setValue];
};

در مرحله بعد، کامپوننت ProtectedRoute را می‌سازیم، که state کاربر فعلی را از هوک useAuth بررسی می‌کند و در صورت عدم احراز هویت، آن‌ها را به صفحه اصلی هدایت می‌کند:

import { Navigate } from "react-router-dom";
import { useAuth } from "../hooks/useAuth";

export const ProtectedRoute = ({ children }) => {
  const { user } = useAuth();
  if (!user) {
    // user is not authenticated
    return <Navigate to="/login" />;
  }
  return children;
};

در کد بالا، ما از کامپوننت <Navigate /> React Router برای هدایت کاربران احراز هویت نشده به مسیر /login استفاده می‌نماییم.

اکنون، گام بعدی اضافه کردن یک مسیر LoginPage برای احراز هویت کاربر و یک مسیر Secret است که فقط برای کاربران وارد شده قابل مشاهده می‌باشد.

برای این کار، یک فایل به نام Login.jsx در دایرکتوری pages خود ایجاد می‌کنیم و کد زیر را در آن قرار می‌دهیم:

import { useState } from "react";
import { useAuth } from "../hooks/useAuth";
export const LoginPage = () => {
  const [username, setUsername] = useState("");
  const [password, setPassword] = useState("");
  const { login } = useAuth();
  const handleLogin = async (e) => {
    e.preventDefault();
    // Here you would usually send a request to your backend to authenticate the user
    // For the sake of this example, we're using a mock authentication
    if (username === "user" && password === "password") {
      // Replace with actual authentication logic
      await login({ username });
    } else {
      alert("Invalid username or password");
    }
  };
  return (
    <div>
      <form onSubmit={handleLogin}>
        <div>
          <label htmlFor="username">Username:</label>
          <input
            id="username"
            type="text"
            value={username}
            onChange={(e) => setUsername(e.target.value)}
          />
        </div>
        <div>
          <label htmlFor="password">Password:</label>
          <input
            id="password"
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          />
        </div>
        <button type="submit">Login</button>
      </form>
    </div>
  );
};

این کامپوننت به عنوان interface ورود به سیستم عمل می‌کند و از هوک useAuth برای کنترل احراز هویت کاربر استفاده می‌نماید. هنگامی که کاربران اطلاعات خود را وارد کرده و فرم را ارسال می‌کنند، تابع login() از useAuth برای احراز هویت و ورود آن‌ها فراخوانی می‌شود.

به طور مشابه، یک فایل Secret.jsx در دایرکتوری pages ایجاد می‌کنیم که یک صفحه ایمن را نشان می‌دهد و محتوا آن منحصراً برای کاربران تأیید شده نمایش داده می‌شود:

import { useAuth } from "../hooks/useAuth";

export const Secret = () => {
  const { logout } = useAuth();

  const handleLogout = () => {
    logout();
  };

  return (
    <div>
      <h1>This is a Secret page</h1>
      <button onClick={handleLogout}>Logout</button>
    </div>
  );
};

همچنین یک دکمه خروج در صفحه Secret در بالا اضافه قرار داده‌ایم تا این امکان را برای کاربران فراهم کنیم که در صورت لزوم از سیستم خارج شوند. این عمل خروج توسط متد logout() از هوک useAuth انجام می‌شود.

در نهایت، در فایل App.jsx، تمام مسیرهای موجود در AuthProvider را از هوک useAuth که قبلا ایجاد کرده بودیم کپسوله می‌کنیم تا یک context احراز هویت ثابت در سراسر برنامه‌ای که داریم فراهم شود. مسیرهای خود را طبق روش همیشگی تنظیم می‌کنیم و برای مسیرهایی که نیاز به احراز هویت دارند، از کامپوننت <ProtectedRoute /> استفاده می‌کنیم تا دسترسی را فقط به کاربران تأیید شده محدود نماییم:

// src/App.jsx

import { Routes, Route } from "react-router-dom";
import { LoginPage } from "./pages/Login";
import { HomePage } from "./pages/Home";
import { Secret } from "./pages/Secret";

import "./App.css";
import { ProtectedRoute } from "./components/ProtectedRoute";
import { AuthProvider } from "./hooks/useAuth";

function App() {
  return (
    <AuthProvider>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/login" element={<LoginPage />} />
        <Route
          path="/secret"
          element={
            <ProtectedRoute>
              <Secret />
            </ProtectedRoute>
          }
        />
      </Routes>
    </AuthProvider>
  );
}

export default App;

با انجام این مراحل، یک جریان احراز هویت اولیه ایجاد کرده‌ایم که فقط به کاربران احراز هویت شده اجازه دسترسی به مسیرهای محافظت شده را می‌دهد. اگر سعی کنیم بدون ورود به سیستم به مسیر /secret دسترسی پیدا کنید، فوراً به صفحه ورود هدایت می‌شویم. با این حال، پس از وارد کردن نام کاربری و رمز عبور پیش‌فرض در فرم ورود، می‌توانیم به صفحه secret دسترسی پیدا کنیم.

اگر تعداد محدودی از مسیرهای محافظت شده داشته باشیم، رویکرد فوق به خوبی کار می‌کند. با این حال، اگر چندین مسیر با این شرایط داشته باشیم، باید هر کدام را wrap کنیم که این موضوع می‌تواند کمی خسته‌کننده باشد.

برای رفع این مشکل، می‌توانیم از ویژگی مسیر تودرتو نسخه ۶ React Router استفاده کنیم تا تمام مسیرهای محافظت‌شده را در یک layout واحد قرار دهیم.

پیاده‌سازی احراز هویت دو مرحله‌ای با استفاده از نسخه ۶ React Router

در این بخش قصد داریم تا برنامه خود را با افزودن احراز هویت دو مرحله‌ای (۲FA) با React Router ارتقا دهیم. ۲FA با الزام کاربران به ارائه دو شکل مجزا از شناسایی قبل از دسترسی به ویژگی‌های حساس، یک لایه امنیتی اضافی اضافه می‌کند.

برای ادامه، ابتدا لازم است تا تنظیمات احراز هویت موجود را تغییر دهیم تا شامل ۲FA شود. فایل useAuth.jsx خود را با کد زیر به‌روزرسانی می‌نماییم:

// src/hooks/useAuth.jsx

import { createContext, useContext, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useLocalStorage } from "./useLocalStorage";

const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useLocalStorage("user", null);
  const [is2FAVerified, setIs2FAVerified] = useState(false);
  const navigate = useNavigate();

  const login = async (data) => {
    setUser(data);

    // Navigate to 2FA verification page
    navigate("/verify-2fa");
  };

  const logout = () => {
    setUser(null);
    setIs2FAVerified(false);
    navigate("/", { replace: true });
  };

  const verify2FACode = async (code) => {
    // Mock verification logic
    if (code === "0000") {
      setIs2FAVerified(true);
      navigate("/secret"); // Navigate to a protected route after successful 2FA
      return true;
    }
    return false;
  };

  const value = {
    user,
    is2FAVerified,
    login,
    logout,
    verify2FACode,
  };

  return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};

export const useAuth = () => {
  return useContext(AuthContext);
};

در این فایل آپدیت شده، ما متد ورود به سیستم را برای ۲FA تغییر داده‌ایم و یک تابع verify2FACode() اضافه کرده‌ایم که کد ۰۰۰۰ را برای سادگی تایید می‌کند. اگر یک سناریوی واقعی را در نظر بگیریم، اینجا جایی است که ما باید تأیید واقعی ۲FA را اجرا نماییم، مانند ارسال یک کد از طریق پیامک یا ایمیل.

در مرحله بعد، یک کامپوننت صفحه جدید اضافه می‌کنیم که به کاربران این امکان را می‌دهد تا کد ۲FA ارسال شده به آن‌ها را در آن وارد کنند:

import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../hooks/useAuth";

export const Verify2FA = () => {
  const navigate = useNavigate();
  const { verify2FACode } = useAuth();
  const [code, setCode] = useState("");

  const handleSubmit = async (e) => {
    e.preventDefault();
    const isValid = await verify2FACode(code);
    if (isValid) {
      navigate("/secret");
    } else {
      alert("Invalid code. Please try again.");
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type= "text"
        value={code}
        onChange={(e) => setCode(e.target.value)}
        placeholder= "Enter verification code"
      />
      <button type="submit">Verify</button>
    </form>
  );
};

ما همچنین باید کامپوننت ProtectedRoute خود را به‌روزرسانی کنیم تا منطق تأیید ۲FA را یکپارچه نماییم:

import { Navigate } from "react-router-dom";
import { useAuth } from "../hooks/useAuth";

export const ProtectedRoute = ({ children }) => {
  const { user, is2FAVerified } = useAuth();

  if (!user) {
    return <Navigate to="/login" />;
  }
  if (!is2FAVerified) {
    return <Navigate to="/verify-2fa" />;
  }

  return children;
};

export default ProtectedRoute;

با راه‌اندازی کامپوننت Verify2FA جدید و کامپوننت ProtectedRoute، تنظیمات مسیر App.jsx خود را تغییر می‌دهیم تا تعریف مسیر verify-2fa را شامل شود، مانند کد زیر:

import { Routes, Route } from "react-router-dom";
import { LoginPage } from "./pages/Login";
import { HomePage } from "./pages/Home";
import { Secret } from "./pages/Secret";
import { Verify2FA } from "./pages/Verify2FA";

import "./App.css";
import { ProtectedRoute } from "./components/ProtectedRoute";
import { AuthProvider } from "./hooks/useAuth";

function App() {
  return (
    <AuthProvider>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/verify-2fa" element={<Verify2FA />} />
        <Route path="/login" element={<LoginPage />} />
        <Route
          path="/secret"
          element={
            <ProtectedRoute>
              <Secret />
            </ProtectedRoute>
          }
        />
      </Routes>
    </AuthProvider>
  );
}

export default App;

پس از راه‌اندازی مجدد برنامه، کاربران احراز هویت نشده هنگامی که سعی می‌کنند به مسیر محافظت شده /secret دسترسی پیدا کنند به صفحه ورود هدایت می‌شوند. همچنین، اگر وارد سیستم شده‌اند اما هنوز ۲FA را نگذرانده‌اند، در این صورت، به صفحه تأیید ۲FA هدایت می‌گردند. کاربران تنها زمانی می‌توانند به مسیرهای محافظت شده دسترسی داشته باشند که هر دو مرحله احراز هویت را تکمیل کرده باشند.

ادغام Auth0 با React Router

یکی دیگر از الگوهای رایج احراز هویت، ادغام React Router با کتابخانه‌های احراز هویت third-party مانند Auth0 است. این فرآیند شامل ایجاد یک حساب کاربری Auth0، بازیابی اطلاعات کاربری و استفاده از کتابخانه‌هایی مانند auth0-react برای اجرای یکپارچه فرآیند احراز هویت می‌باشد.

از آن جایی که ادغام Auth0 با React Router خارج از محدوده این مقاله است، یادگیری انجام این کار با مطالعه مستندات رسمی Auth0 می‌تواند بسیار مفید باشد.

استفاده از مسیرهای تودرتو و <Outlet />

یکی از قدرتمندترین ویژگی‌های نسخه ۶ React Router، مسیرهای تودرتو است. این ویژگی به ما این امکان را می‌دهد تا مسیری داشته باشیم که شامل مسیرهای child دیگری باشد. اکثر layoutهای ما با بخش‌هایی در URL همراه هستند، و React Router به طور کامل از آن پشتیبانی می‌کند.

برای مثال، می‌توانیم یک کامپوننت parent <Route /> را به مسیرهای <HomePage /> و <LoginPage /> اضافه نماییم، مانند:

import { ProtectedLayout } from "./components/ProtectedLayout";
import { HomeLayout } from "./components/HomeLayout";
// ...

export default function App() {
  return (
    <Routes>
      <Route element={<HomeLayout />}>
        <Route path="/" element={<HomePage />} />
        <Route path="/login" element={<LoginPage />} />
      </Route>

      <Route path="/dashboard" element={<ProtectedLayout />}>
        <Route path="profile" element={<ProfilePage />} />
        <Route path="settings" element={<SettingsPage />} />
      </Route>
    </Routes>
  );
}

کامپوننت parent <Route /> نیز می‌تواند یک path داشته باشد و مسئول رندر کردن کامپوننت child <Route /> در صفحه است.

هنگامی که کاربر به /dashboard/profile می‌رود، روتر <ProfilePage /> را نمایش می‌دهد. برای اینکه این اتفاق بیفتد، المنت route مربوط به parent باید یک کامپوننت <Outlet /> برای رندر کردن المنت‌های child داشته باشد. کامپوننت Outlet به المنت‌های تودرتو در رابط کاربری این امکان را می‌دهد تا هنگام نمایش مسیرهای child قابل مشاهده باشند.

المنت route مربوط به parent همچنین می‌تواند منطق business مشترک و رابط کاربری اضافی داشته باشد. به عنوان مثال، در کامپوننت <ProtectedLayout />، منطق مسیر خصوصی را به همراه یک navigation bar مشترک قرار داده‌ایم که هنگام نمایش مسیرهای child قابل مشاهده خواهد بود:

import { Navigate, Outlet } from "react-router-dom";
import { useAuth } from "../hooks/useAuth";

export const ProtectedLayout = () => {
  const { user } = useAuth();

  if (!user) {
    return <Navigate to="/" />;
  }

  return (
    <div>
      <nav>
        <Link to="/settings">Settings</Link>
        <Link to="/profile">Profile</Link>
      </nav>
      <Outlet />
    </div>
  )
};

به جای کامپوننت <Outlet />، می‌توانیم از هوک useOutlet نیز استفاده کنیم که همان هدف را دنبال می‌کند:

import { Link, Navigate, useOutlet } from "react-router-dom";
// ...

export const ProtectedLayout = () => {
  const { user } = useAuth();
  const outlet = useOutlet();

  if (!user) {
    return <Navigate to="/" />;
  }

  return (
    <div>
      <nav>
        <Link to="/settings">Settings</Link>
        <Link to="/profile">Profile</Link>
      </nav>
      {outlet}
    </div>
  );
};

مشابه مسیرهای محافظت شده، ما نمی‌خواهیم کاربران احراز هویت شده به path /login دسترسی داشته باشند. بنابراین، برای مدیریت آن در کامپوننت <HomeLayout /> به صورت زیر عمل می‌کنیم:

import { Navigate, Outlet } from "react-router-dom";
import { useAuth } from "../hooks/useAuth";

export const HomeLayout = () => {
  const { user } = useAuth();

  if (user) {
    return <Navigate to="/dashboard/profile" />;
  }

  return (
    <div>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/login">Login</Link>
      </nav>
      <Outlet />
    </div>
  )
};

دسترسی به کد و دمو برنامه‌ای که نوشتیم از طریق این لینک امکان‌پذیر می‌باشد.

استفاده از APIهای data library نسخه ۶٫۴ React Router

پکیج React Router در نسخه ۶٫۴، روترها و data APIهای جدیدی را معرفی کرد. همه برنامه‌های وب برای فعال کردن دسترسی به data API باید از تابع createBrowserRouter() استفاده کنند. سریع‌ترین راه برای به‌روزرسانی یک برنامه موجود به API جدید React Router این است که کامپوننت‌های Route را درون تابع createRoutesFromElements() قرار دهیم:

export const router = createBrowserRouter(
  createRoutesFromElements(
    <>
      <Route element={<HomeLayout />}>
        <Route path="/" element={<HomePage />} />
        <Route path="/login" element={<LoginPage />} />
      </Route>

      <Route path="/dashboard" element={<ProtectedLayout />}>
        <Route path="profile" element={<ProfilePage />} />
        <Route path="settings" element={<SettingsPage />} />
      </Route>
    </>
  )
);

در فایل index.js، به جای کامپوننت <BrowserRouter />، از کامپوننت <RouterProvider /> استفاده می‌کنیم و آبجکت router export شده را از فایل App.js ارسال می‌نماییم. همچنین باید به این نکته توجه داشته باشیم که AuthProvider بدون BrowserRouter کار نخواهد کرد زیرا از تابع useNavigate() استفاده می‌کند:

import { router } from "./App";
...
root.render(
  <StrictMode>
    <ThemeProvider theme={theme}>
      <RouterProvider router={router} />
    </ThemeProvider>
  </StrictMode>
);

برای استفاده از AuthProvider در context روتر، باید یک کامپوننت <AuthLayout /> ایجاد کنیم که المنت outlet را درون AuthProvider قرار دهد. این کار تمام مسیرهای child را قادر می‌سازد تا به AuthContext دسترسی داشته باشند:

import { useLoaderData, useOutlet } from "react-router-dom";
import { AuthProvider } from "../hooks/useAuth";

export const AuthLayout = () => {
  const outlet = useOutlet();

  return (
    <AuthProvider>{outlet}</AuthProvider>
  );
};

اکنون می‌توانیم از کامپوننت AuthLayout به عنوان یک مسیر در سطح root استفاده کنیم، به عنوان مثال:

export const router = createBrowserRouter(
  createRoutesFromElements(
    <Route
      element={<AuthLayout />}
    >
      <Route element={<HomeLayout />}>
        ...
      </Route>

      <Route path="/dashboard" element={<ProtectedLayout />}>
        ...
      </Route>
    </Route>
  )
);

در این مرحله، برنامه برای دسترسی به data APIها آماده است.

با data APIهای React Router، می‌توانیم نحوه دریافت داده‌ها را انتزاعی کنیم. معمولاً، ما داده‌ها را در داخل کامپوننت خود با استفاده از هوک useEffect لود می‌کنیم. در عوض، می‌توانیم از تابع loader() Router برای دریافت داده‌ها قبل از رندر المنت route استفاده نماییم.

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

برای شبیه سازی دریافت داده‌ها، می‌توانیم از Promise با متد setTimeout() استفاده کنیم و user را از localStorage دریافت نماییم:

const getUserData = () =>
  new Promise((resolve) =>
    setTimeout(() => {
      const user = window.localStorage.getItem("user");
      resolve(user);
    }, ۳۰۰۰)
  );

با استفاده از prop loader در کامپوننت Route، می‌توانیم Promise – getUserData() – را به کامپوننت AuthLayout با کمک تابع utility defer() ارسال کنیم. تابع defer() به ما این امکان را می‌دهد که قبل از رندر شدن کامپوننت Route به جای مقادیر resolve شده، promiseها را ارسال کنیم:

import {
  Route,
  createBrowserRouter,
  createRoutesFromElements,
  defer
} from "react-router-dom";

import { AuthLayout } from "./components/AuthLayout";
...
// ideally this would be an API call to server to get logged in user data
const getUserData = () =>
  new Promise((resolve) =>
    setTimeout(() => {
      const user = window.localStorage.getItem("user");
      resolve(user);
    }, ۳۰۰۰)
  );

export const router = createBrowserRouter(
  createRoutesFromElements(
    <Route
      element={<AuthLayout />}
      loader={() => defer({ userPromise: getUserData() })}
    >
      <Route element={<HomeLayout />}>
        ...
      </Route>

      <Route path="/dashboard" element={<ProtectedLayout />}>
        ...
      </Route>
    </Route>
  )
);

در کامپوننت AuthLayout، می‌توانیم با استفاده از هوک useLoaderData به userPromise دسترسی داشته باشیم.

کامپوننت Await می‌تواند مقادیر معوق را با مکانیزم مدیریت خطای داخلی رندر کند. کامپوننت Await باید داخل React Suspense قرار بگیرد تا یک رابط کاربری fallback فعال شود. در این مورد، ما یک progress bar خطی را رندر می‌کنیم تا زمانی که userPromise که داریم resolve شود.

ما می‌توانیم یک کامپوننت را به prop errorElement ارسال کنیم تا در صورت rejecte شدن Promise، بتوانیم state رابط کاربری خطا را رندر نماییم.

در نهایت، می‌توانیم داده‌های کاربر را به عنوان مقدار اولیه به AuthProvider ارسال کنیم:

import { Suspense } from "react";
import { useLoaderData, useOutlet, Await } from "react-router-dom";
import LinearProgress from "@mui/material/LinearProgress";
import Alert from "@mui/material/Alert";
import { AuthProvider } from "../hooks/useAuth";

export const AuthLayout = () => {
  const outlet = useOutlet();

  const { userPromise } = useLoaderData();

  return (
    <Suspense fallback={<LinearProgress />}>
      <Await
        resolve={userPromise}
        errorElement={<Alert severity="error">Something went wrong!</Alert>}
        children={(user) => (
          <AuthProvider userData={user}>{outlet}</AuthProvider>
        )}
      />
    </Suspense>
  );
};

برای تأیید شرایط خطا، می‌توانیم Promise را مطابق شکل زیر reject کنیم:

// for error
const getUserData = () =>
  new Promise((resolve, reject) =>
    setTimeout(() => {
      reject("Error");
    }, ۳۰۰۰)
  );

دسترسی به کد کامل نمونه احراز هویت ۲FA در لینک GitHub و نمونه کد ادغام data library در این لینک امکان‌پذیر می‌باشد.

جمع‌بندی

در این مقاله سعی کردیم نحوه مدیریت احراز هویت کاربران با استفاده از نسخه ۶ React Router را بررسی کنیم. نسخه ۶ React Router نسبت به نسخه‌های قبلی پیشرفت زیادی داشته است و سریع، پایدار و قابل اعتماد می‌باشد. علاوه بر این که کار کردن با آن بسیار آسان است، دارای بسیاری از ویژگی‌های جدید مانند <Outlet /> و یک کامپوننت بهبود یافته <Route /> است که مسیریابی را در برنامه‌های React بسیار ساده‌تر کرده است.

با routerها و data APIهای جدید موجود در نسخه ۶٫۴، می‌توانیم به راحتی stateهای optimistic UI، pending و error را مدیریت کنیم. همچنین، می‌توانیم در حین نمایش یک رابط کاربری fallback تا زمانی که داده‌ها آماده شوند، داده‌های خارج از کامپوننت را abstract کرده و لود نماییم.