پیاده‌سازی SSR در ۷ React Router

React Router مدت‌هاست که به عنوان یک راه‌حل محبوب برای مسیریابی در برنامه‌های تک‌صفحه‌ای (SPA) شناخته می‌شود. تیم توسعه‌دهنده Remix با بهبودهای پیوسته در این کتابخانه، React Router و Remix را به هم نزدیک‌تر کرده‌اند که در نهایت منجر به ادغام آن‌ها در ۷ React Router شده است. با این نسخه‌ی جدید، SSR در ۷ React Router نیز امکان‌پذیر شده و این کتابخانه می‌تواند به عنوان یک کتابخانه مسیریابی یا یک فریم‌ورک کامل استفاده شود که تمام قابلیت‌های Remix را دربرمی‌گیرد. همچنین، این نسخه شامل نسخه ۱۹ React به عنوان وابستگی نیز می‌باشد.

در این مقاله، نحوه ساخت یک برنامه رندرینگ سمت سرور (SSR) با React Router 7 را با ساخت یک اپلیکیشن book tracking بررسی خواهیم کرد. در این مسیر، از ابزارهایی مانند Radix Primitives، React Icons و Tailwind CSS استفاده می‌کنیم. داشتن دانش قبلی در مورد React.js، تایپ اسکریپت و مفاهیم پایه‌ای دریافت داده مانند actionها و loaderها مفید است، اما ضروری نیست. کد نهایی پروژه در این لینک قابل دسترس می‌باشد.

نحوه‌ی راه‌اندازی فریم‌ورک React Router

حداقل نسخه‌ مورد نیاز برای اجرای React Router، نسخه ۲۰ Node.js است. بنابراین، باید مطمئن شویم که دستگاه ما از این نسخه یا بالاتر استفاده می‌کند:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
node --version
node --version
node --version

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
npx create-react-router@latest react-router-ssr
npx create-react-router@latest react-router-ssr
npx create-react-router@latest react-router-ssr

در اینجا، نام پروژه نمونه ما

react-router-ssr
react-router-ssr است. پس از اجرای دستور فوق، CLI از ما می‌پرسد که آیا می‌خواهیم یک git repo برای پروژه ایجاد کنیم یا خیر. در صورت تمایل، گزینه «yes» را انتخاب می‌کنیم. همچنین، از ما می‌پرسد که آیا می‌خواهیم وابستگی‌ها را با استفاده از npm نصب نماییم یا نه. پس از انتخاب گزینه‌های مورد نظر، پوشه‌ای با نام پروژه ما ایجاد می‌شود. به دایرکتوری پروژه می‌رویم و سرور توسعه برنامه را اجرا می‌کنیم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
cd react-router-ssr
npm run dev
cd react-router-ssr npm run dev
cd react-router-ssr
npm run dev

سپس، مرورگر خود را باز کرده و به آدرس

http://localhost:5173
http://localhost:5173 می‌رویم. باید صفحه اصلی برنامه را مشاهده نماییم.

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

.dockerignore
.dockerignore،
Dockerfile
Dockerfile،
Dockerfile.bun
Dockerfile.bun و
Dockerfile.pnpm
Dockerfile.pnpm هستند که در قالب پروژه برای مواردی که نیاز به استقرار با Docker است، گنجانده شده‌اند.

نحوه ساخت صفحات SSR

به منظور استفاده از ۷ React Router برای SSR، باید مطمئن شویم که گزینه

ssr
ssr در فایل پیکربندی React Router روی
true
true تنظیم شده باشد. به طور پیش‌فرض، این مقدار روی
true
true قرار دارد. فایل
react-router.config.ts
react-router.config.ts را در ویرایشگر کد خود باز کرده و بررسی می‌کنیم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
//router.config.ts
import type { Config } from '@react-router/dev/config';
export default {
// Config options...
// Server-side render by default, to enable SPA mode set this to `false`
ssr: true,
} satisfies Config;
//router.config.ts import type { Config } from '@react-router/dev/config'; export default { // Config options... // Server-side render by default, to enable SPA mode set this to `false` ssr: true, } satisfies Config;
//router.config.ts

import type { Config } from '@react-router/dev/config';

export default {
  // Config options...
  // Server-side render by default, to enable SPA mode set this to `false`
  ssr: true,
} satisfies Config;

این آموزش از تم «light mode» برای برنامه استفاده می‌کند، بنابراین باید dark mode را در Tailwind CSS غیرفعال نماییم. فایل

app/app.css
app/app.css را باز کرده و تمام استایل‌های مربوط به «dark mode» را غیرفعال می‌کنیم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// routes/app.css
@tailwind base;
@tailwind components;
@tailwind utilities;
html,
body {
/* @apply bg-white dark:bg-gray-950; */
@media (prefers-color-scheme: dark) {
/* color-scheme: dark; */
}
}
// routes/app.css @tailwind base; @tailwind components; @tailwind utilities; html, body { /* @apply bg-white dark:bg-gray-950; */ @media (prefers-color-scheme: dark) { /* color-scheme: dark; */ } }
// routes/app.css

@tailwind base;
@tailwind components;
@tailwind utilities;

html,
body {
  /* @apply bg-white dark:bg-gray-950; */
  @media (prefers-color-scheme: dark) {
    /* color-scheme: dark; */
  }
}

سپس، اولین صفحه SSR خود را می‌سازیم. تمام routeها را در پوشه

app/routes/
app/routes/ تعریف می‌کنیم، اما
home.tsx
home.tsx به عنوان صفحه اصلی عمل خواهد کرد. همچنین، مسیرهای دیگری وجود خواهند داشت که از آن به عنوان layout استفاده می‌کنند. فایل
app/routes/home.tsx
app/routes/home.tsx را ایجاد می‌کنیم.

در فایل

app/routes/home.tsx
app/routes/home.tsx، کامپوننت
<Home />
<Home /> را export می‌کنیم که شامل موارد زیر می‌باشد:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// app/routes/home.tsx
import { Outlet } from 'react-router';
import { Fragment } from 'react/jsx-runtime';
import Header from '~/components/Header';
import Footer from '~/components/Footer';
export default function Home() {
return (
<Fragment>
<Header />
<main className='max-w-screen-lg mx-auto my-4'>
<Outlet />
</main>
<Footer />
</Fragment>
);
}
// app/routes/home.tsx import { Outlet } from 'react-router'; import { Fragment } from 'react/jsx-runtime'; import Header from '~/components/Header'; import Footer from '~/components/Footer'; export default function Home() { return ( <Fragment> <Header /> <main className='max-w-screen-lg mx-auto my-4'> <Outlet /> </main> <Footer /> </Fragment> ); }
// app/routes/home.tsx

import { Outlet } from 'react-router';
import { Fragment } from 'react/jsx-runtime';
import Header from '~/components/Header';
import Footer from '~/components/Footer';

export default function Home() {
  return (
    <Fragment>
      <Header />
      <main className='max-w-screen-lg mx-auto my-4'>
        <Outlet />
      </main>
      <Footer />
    </Fragment>
  );
}

این فایل دو کامپوننت React (

<Header />
<Header /> و
<Footer />
<Footer />) را که در ادامه ایجاد خواهیم کرد، و کامپوننت
<Outlet />
<Outlet /> از React Router را import می‌کند.
<Outlet />
<Outlet /> کامپوننت‌های هر مسیر child را که از
home.tsx
home.tsx به عنوان layout استفاده می‌کند، رندر می‌نماید.

برای نمایش چیزی در صفحه، باید کامپوننت‌های سفارشی import شده را ایجاد کنیم. با اصلاح پوشه

app/welcome
app/welcome که همراه با layout است، شروع می‌کنیم:

  • یا پوشه
    app/welcome
    app/welcome را حذف کرده و یک پوشه جدید به نام
    app/components
    app/components می‌سازیم،
  • و یا نام پوشه
    welcome
    welcome را به
    components
    components تغییر داده و تمام فایل‌های داخل آن را حذف می‌نماییم.

سپس، در پوشه

app/components
app/components، دو فایل جدید به نام‌های
Header.tsx
Header.tsx و
Footer.tsx
Footer.tsx ایجاد می‌کنیم.

کامپوننت

<Header />
<Header /> یک
<header>
<header> را نمایش می‌دهد که برای بیشتر بخش‌های برنامه ثابت است. کد آن به صورت زیر می‌باشد:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// app/components/Header.tsx
import { Link } from 'react-router';
import BookForm from './BookForm';
export default function Header() {
return (
<header className='flex justify-between items-center px-8 py-4'>
<h1 className='text-3xl font-medium'>
<Link to='/'>Book Tracker App</Link>
</h1>
<BookForm />
</header>
);
}
// app/components/Header.tsx import { Link } from 'react-router'; import BookForm from './BookForm'; export default function Header() { return ( <header className='flex justify-between items-center px-8 py-4'> <h1 className='text-3xl font-medium'> <Link to='/'>Book Tracker App</Link> </h1> <BookForm /> </header> ); }
// app/components/Header.tsx

import { Link } from 'react-router';
import BookForm from './BookForm';

export default function Header() {
  return (
    <header className='flex justify-between items-center px-8 py-4'>
      <h1 className='text-3xl font-medium'>
        <Link to='/'>Book Tracker App</Link>
      </h1>
      <BookForm />
    </header>
  );
}

فایل

Header.tsx
Header.tsx کامپوننت
<Link />
<Link /> را از React Router ایمپورت کرده است که در واقع یک تگ
<a>
<a> بهینه شده برای این فریم‌ورک می‌باشد. همچنین، کامپوننت
<BookForm />
<BookForm /> را که هنوز وجود ندارد، import کرده‌ایم. در نهایت، این فایل از استایل‌های Tailwind CSS استفاده می‌کند تا المنت‌های HTML در صفحه ظاهر مناسبی داشته باشند.

ایجاد کامپوننت
<BookForm />
<BookForm />

اکنون باید کامپوننت

<BookForm />
<BookForm /> را ایجاد کنیم. اما قبل از آن، لازم است که کامپوننت headless dialog از Radix را نصب کنیم. از این کامپوننت برای ساخت یک فرم دیالوگ جهت اضافه کردن کتاب جدید به سیستم ردیابی استفاده خواهیم کرد. همچنین، React Icons را نصب می‌کنیم، چون در برخی از کامپوننت‌های آینده به آن نیاز داریم.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
npm install @radix-ui/react-dialog react-icons
npm install @radix-ui/react-dialog react-icons
npm install @radix-ui/react-dialog react-icons

بعد از نصب این پکیج‌ها، یک فایل جدید در مسیر

app/components
app/components با نام
BookForm.tsx
BookForm.tsx می‌سازیم و کد زیر را در آن قرار می‌دهیم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// app/components/BookForm.tsx
import { useState } from 'react';
import { Form } from 'react-router';
import * as Dialog from '@radix-ui/react-dialog';
import Button from './Button';
export default function BookForm() {
const [isOpen, setIsOpen] = useState<boolean>(false);
return (
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
<Dialog.Trigger asChild>
<Button>Add Book</Button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className='bg-black/50 fixed inset-0' />
<Dialog.Content className='bg-white fixed top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2 px-8 py-4 w-5/6 max-w-sm'>
<Dialog.Title className='font-medium text-xl py-2'>
Add New Book
</Dialog.Title>
<Dialog.Description>Start tracking a new book</Dialog.Description>
<Form
method='post'
onSubmit={() => setIsOpen(false)}
action='/?index'
className='mt-2'
>
<div>
<label htmlFor='title'>Book Title</label>
<br />
<input
name='title'
type='text'
className='border border-black'
id='title'
required
/>
</div>
<div>
<label htmlFor='author'>Author</label>
<br />
<input
name='author'
type='text'
id='author'
className='border border-black'
required
/>
</div>
<div>
<label htmlFor='isbn'>ISBN (Optional)</label>
<br />
<input
name='isbn'
type='text'
id='isbn'
className='border border-black'
/>
</div>
<div className='mt-4 text-right'>
<Dialog.Close asChild>
<Button variant='cancel'>Cancel</Button>
</Dialog.Close>
<Button type='submit'>Save</Button>
</div>
</Form>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
// app/components/BookForm.tsx import { useState } from 'react'; import { Form } from 'react-router'; import * as Dialog from '@radix-ui/react-dialog'; import Button from './Button'; export default function BookForm() { const [isOpen, setIsOpen] = useState<boolean>(false); return ( <Dialog.Root open={isOpen} onOpenChange={setIsOpen}> <Dialog.Trigger asChild> <Button>Add Book</Button> </Dialog.Trigger> <Dialog.Portal> <Dialog.Overlay className='bg-black/50 fixed inset-0' /> <Dialog.Content className='bg-white fixed top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2 px-8 py-4 w-5/6 max-w-sm'> <Dialog.Title className='font-medium text-xl py-2'> Add New Book </Dialog.Title> <Dialog.Description>Start tracking a new book</Dialog.Description> <Form method='post' onSubmit={() => setIsOpen(false)} action='/?index' className='mt-2' > <div> <label htmlFor='title'>Book Title</label> <br /> <input name='title' type='text' className='border border-black' id='title' required /> </div> <div> <label htmlFor='author'>Author</label> <br /> <input name='author' type='text' id='author' className='border border-black' required /> </div> <div> <label htmlFor='isbn'>ISBN (Optional)</label> <br /> <input name='isbn' type='text' id='isbn' className='border border-black' /> </div> <div className='mt-4 text-right'> <Dialog.Close asChild> <Button variant='cancel'>Cancel</Button> </Dialog.Close> <Button type='submit'>Save</Button> </div> </Form> </Dialog.Content> </Dialog.Portal> </Dialog.Root> ); }
// app/components/BookForm.tsx

import { useState } from 'react';
import { Form } from 'react-router';
import * as Dialog from '@radix-ui/react-dialog';
import Button from './Button';

export default function BookForm() {
  const [isOpen, setIsOpen] = useState<boolean>(false);

  return (
    <Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
      <Dialog.Trigger asChild>
        <Button>Add Book</Button>
      </Dialog.Trigger>
      <Dialog.Portal>
        <Dialog.Overlay className='bg-black/50 fixed inset-0' />
        <Dialog.Content className='bg-white fixed top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2 px-8 py-4 w-5/6 max-w-sm'>
          <Dialog.Title className='font-medium text-xl py-2'>
            Add New Book
          </Dialog.Title>
          <Dialog.Description>Start tracking a new book</Dialog.Description>
          <Form
            method='post'
            onSubmit={() => setIsOpen(false)}
            action='/?index'
            className='mt-2'
          >
            <div>
              <label htmlFor='title'>Book Title</label>
              <br />
              <input
                name='title'
                type='text'
                className='border border-black'
                id='title'
                required
              />
            </div>
            <div>
              <label htmlFor='author'>Author</label>
              <br />
              <input
                name='author'
                type='text'
                id='author'
                className='border border-black'
                required
              />
            </div>
            <div>
              <label htmlFor='isbn'>ISBN (Optional)</label>
              <br />
              <input
                name='isbn'
                type='text'
                id='isbn'
                className='border border-black'
              />
            </div>
            <div className='mt-4 text-right'>
              <Dialog.Close asChild>
                <Button variant='cancel'>Cancel</Button>
              </Dialog.Close>
              <Button type='submit'>Save</Button>
            </div>
          </Form>
        </Dialog.Content>
      </Dialog.Portal>
    </Dialog.Root>
  );
}

کامپوننت

BookForm.tsx
BookForm.tsx از هوک
useState
useState
 برای کنترل نمایش و بسته شدن دیالوگ استفاده می‌کند. همچنین، از Tailwind CSS برای استایل‌دهی و از React Icons برای نمایش آیکون (+) بهره می‌برد.

ایجاد کامپوننت
<Button />
<Button />

در بخش قبل در کد

BookForm.tsx
BookForm.tsx، ما یک کامپوننت
<Button />
<Button /> را import کرده‌ایم، اما هنوز آن را نساخته‌ایم. اکنون باید این کامپوننت را در مسیر
app/components/Button.tsx
app/components/Button.tsx بسازیم.

کد کامپوننت

<Button />
<Button /> به صورت زیر می‌باشد:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// app/components/Button.tsx
import type { ComponentProps, ReactNode } from 'react';
interface Props extends ComponentProps<'button'> {
children?: ReactNode;
variant?: 'cancel' | 'delete' | 'normal';
}
export default function Button({
children,
variant = 'normal',
...otherProps
}: Props) {
const variantStyles: Record<NonNullable<typeof variant>, string> = {
cancel: 'text-red-700',
normal: 'text-white bg-purple-700 hover:bg-purple-800',
delete: 'text-white bg-red-700 hover:bg-red-800',
};
return (
<button
className={`rounded-full px-4 py-2 text-center text-sm ${variantStyles[variant]}`}
{...otherProps}
>
{children}
</button>
);
}
// app/components/Button.tsx import type { ComponentProps, ReactNode } from 'react'; interface Props extends ComponentProps<'button'> { children?: ReactNode; variant?: 'cancel' | 'delete' | 'normal'; } export default function Button({ children, variant = 'normal', ...otherProps }: Props) { const variantStyles: Record<NonNullable<typeof variant>, string> = { cancel: 'text-red-700', normal: 'text-white bg-purple-700 hover:bg-purple-800', delete: 'text-white bg-red-700 hover:bg-red-800', }; return ( <button className={`rounded-full px-4 py-2 text-center text-sm ${variantStyles[variant]}`} {...otherProps} > {children} </button> ); }
// app/components/Button.tsx

import type { ComponentProps, ReactNode } from 'react';

interface Props extends ComponentProps<'button'> {
  children?: ReactNode;
  variant?: 'cancel' | 'delete' | 'normal';
}

export default function Button({
  children,
  variant = 'normal',
  ...otherProps
}: Props) {
  const variantStyles: Record<NonNullable<typeof variant>, string> = {
    cancel: 'text-red-700',
    normal: 'text-white bg-purple-700 hover:bg-purple-800',
    delete: 'text-white bg-red-700 hover:bg-red-800',
  };
  return (
    <button
      className={`rounded-full px-4 py-2 text-center text-sm ${variantStyles[variant]}`}
      {...otherProps}
    >
      {children}
    </button>
  );
}

کامپوننت

<Button />
<Button /> یک دکمه با قابلیت استفاده مجدد را فراهم می‌کند که دارای سه نوع مختلف (
normal
normal،
cancel
cancel و
delete
delete) است. هر کدام از این حالت‌ها دارای استایل خاص خود هستند.

ایجاد کامپوننت
<Footer />
<Footer />

در نهایت، برای تکمیل ساختار اولیه برنامه، باید کامپوننت

<Footer />
<Footer /> را برای مسیر
home.tsx
home.tsx ایجاد کنیم.

کد کامپوننت

<Footer />
<Footer /> به شکل زیر می‌باشد:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// app/components/Footer.tsx
import { Link } from 'react-router';
export default function Footer() {
return (
<footer className='text-center my-5'>
<Link to='/about' className='text-purple-700'>
About the App
</Link>
</footer>
);
}
// app/components/Footer.tsx import { Link } from 'react-router'; export default function Footer() { return ( <footer className='text-center my-5'> <Link to='/about' className='text-purple-700'> About the App </Link> </footer> ); }
// app/components/Footer.tsx

import { Link } from 'react-router';

export default function Footer() {
  return (
    <footer className='text-center my-5'>
      <Link to='/about' className='text-purple-700'>
        About the App
      </Link>
    </footer>
  );
}

این کامپوننت یک footer ساده ایجاد می‌کند که شامل متن کپی‌رایت می‌باشد.

تولید سایت استاتیک (SSG) در نسخه ۷ React Router

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

  1. تولید سایت داینامیک (Dynamic Site Generation): در این روش، سرور برای هر درخواست جدید، صفحه را به صورت داینامیک تولید می‌کند.
  2. تولید سایت استاتیک (Static Site Generation – SSG): در این روش، صفحات از قبل ایجاد شده و روی سرور ذخیره می‌شوند. برای صفحات SSG، محتوای صفحه ثابت است و بدون توجه به اینکه چه کسی درخواست را ارسال کرده، یکسان باقی می‌ماند.

تفاوت SSR و SSG

در SSR داینامیک، سرور هنگام درخواست، صفحه را ایجاد کرده و سپس HTML را به سمت کلاینت (مرورگر) ارسال می‌کند تا در آنجا hydrate شود.
اما در SSG، تمامی فایل‌های موردنیاز یک صفحه (HTML، CSS، JavaScript) در زمان build ایجاد می‌شوند. به همین دلیل، هنگام درخواست، سریع‌تر به کلاینت ارسال می‌گردند، زیرا نیازی به پردازش اضافی روی سرور ندارند.

کدام روش بهتر است؟

هر دو روش مزایا و معایب خاص خود را دارند.
SSG مناسب است اگر:

  • همه کاربران باید محتوای یکسانی را ببینند.
  • صفحه نیازی به به‌روزرسانی مداوم ندارد (مانند صفحات وبلاگ، تماس با ما، درباره ما).
  • می‌خواهیم صفحات را سریع‌تر و با استفاده از CDN بارگذاری نماییم.

SSR داینامیک مناسب است اگر:

  • محتوای صفحه مرتب تغییر می‌کند.
  • هر کاربر باید محتوای خاص خودش را دریافت نماید.

به عنوان مثال، در این پروژه‌ای که داریم مسیر

/about
/about را به صورت SSG ایجاد می‌کنیم.
React Router 7 این امکان را به توسعه‌دهندگان می‌دهد که از هر دو روش (SSR و SSG) در یک پروژه استفاده کنند.

برای این کار، باید پیکربندی React Router را ویرایش کرده و مشخص کنیم که مسیر

/about
/about باید به صورت استاتیک (pre-rendered) ایجاد شود. در این صورت، برنامه فقط مسیر (یا صفحه)
/about
/about را از قبل رندر می‌کند:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// react-router.config.ts
import type { Config } from '@react-router/dev/config';
export default {
// Config options...
// Server-side render by default, to enable SPA mode set this to `false`
ssr: true,
async prerender() {
return ['about'];
},
} satisfies Config;
// react-router.config.ts import type { Config } from '@react-router/dev/config'; export default { // Config options... // Server-side render by default, to enable SPA mode set this to `false` ssr: true, async prerender() { return ['about']; }, } satisfies Config;
// react-router.config.ts

import type { Config } from '@react-router/dev/config';

export default {
  // Config options...
  // Server-side render by default, to enable SPA mode set this to `false`
  ssr: true,
  async prerender() {
    return ['about'];
  },
} satisfies Config;

ایجاد صفحه About

در مسیر

app/routes/about.tsx
app/routes/about.tsx فایل جدید را ایجاد می‌کنیم. این فایل حاوی محتوای استاتیک مربوط به صفحه «درباره ما» خواهد بود:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// app/routes/about.tsx
import { Fragment } from 'react/jsx-runtime';
import { Link } from 'react-router';
export default function About() {
return (
<Fragment>
<h1 className='px-8 py-4 text-3xl font-medium'>
<Link to='/'>Book Tracker App</Link>
</h1>
<main className='max-w-screen-lg mx-auto my-4'>
<p className='mb-2 mx-5'>
This app was built for readers who love the simplicity of tracking
what they’ve read and what they want to read next. With just the
essentials, it’s designed to keep your reading list organized without
the distractions of unnecessary features.
</p>
<p className='mb-2 mx-5'>
We believe the joy of reading should stay front and center. Whether
it’s noting down the books you’ve finished or keeping a simple list of
what’s next, this app focuses on helping you stay connected to your
reading journey in the most straightforward way possible.
</p>
<p className='mb-2 mx-5'>
Sometimes less is more, and that’s the philosophy behind this app. By
keeping things minimal, it offers a clean and easy way to manage your
reading habits so you can spend less time tracking and more time
diving into your next great book.
</p>
</main>
</Fragment>
);
}
// app/routes/about.tsx import { Fragment } from 'react/jsx-runtime'; import { Link } from 'react-router'; export default function About() { return ( <Fragment> <h1 className='px-8 py-4 text-3xl font-medium'> <Link to='/'>Book Tracker App</Link> </h1> <main className='max-w-screen-lg mx-auto my-4'> <p className='mb-2 mx-5'> This app was built for readers who love the simplicity of tracking what they’ve read and what they want to read next. With just the essentials, it’s designed to keep your reading list organized without the distractions of unnecessary features. </p> <p className='mb-2 mx-5'> We believe the joy of reading should stay front and center. Whether it’s noting down the books you’ve finished or keeping a simple list of what’s next, this app focuses on helping you stay connected to your reading journey in the most straightforward way possible. </p> <p className='mb-2 mx-5'> Sometimes less is more, and that’s the philosophy behind this app. By keeping things minimal, it offers a clean and easy way to manage your reading habits so you can spend less time tracking and more time diving into your next great book. </p> </main> </Fragment> ); }
// app/routes/about.tsx

import { Fragment } from 'react/jsx-runtime';
import { Link } from 'react-router';

export default function About() {
  return (
    <Fragment>
      <h1 className='px-8 py-4 text-3xl font-medium'>
        <Link to='/'>Book Tracker App</Link>
      </h1>
      <main className='max-w-screen-lg mx-auto my-4'>
        <p className='mb-2 mx-5'>
          This app was built for readers who love the simplicity of tracking
          what they’ve read and what they want to read next. With just the
          essentials, it’s designed to keep your reading list organized without
          the distractions of unnecessary features.
        </p>
        <p className='mb-2 mx-5'>
          We believe the joy of reading should stay front and center. Whether
          it’s noting down the books you’ve finished or keeping a simple list of
          what’s next, this app focuses on helping you stay connected to your
          reading journey in the most straightforward way possible.
        </p>
        <p className='mb-2 mx-5'>
          Sometimes less is more, and that’s the philosophy behind this app. By
          keeping things minimal, it offers a clean and easy way to manage your
          reading habits so you can spend less time tracking and more time
          diving into your next great book.
        </p>
      </main>
    </Fragment>
  );
}

مسیریابی با React Router

در این بخش، نحوه پیکربندی مسیرها در فریم‌ورک React Router را بررسی خواهیم کرد.

قبل از اینکه بتوانیم صفحه About را در مرورگر مشاهده کنیم، باید React Router را تنظیم نماییم تا هنگام پیمایش به مسیر

/about
/about، ماژول مسیر مربوطه (
about.tsx
about.tsx) را نمایش دهد.

این پیکربندی در فایل

app/routes.ts
app/routes.ts انجام می‌شود، جایی که تمام سلسله‌مراتب مسیرهای برنامه تعریف شده است:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// app/routes.ts
import { type RouteConfig, index, route } from '@react-router/dev/routes';
export default [
index('routes/home.tsx'),
route('about', 'routes/about.tsx'),
] satisfies RouteConfig;
// app/routes.ts import { type RouteConfig, index, route } from '@react-router/dev/routes'; export default [ index('routes/home.tsx'), route('about', 'routes/about.tsx'), ] satisfies RouteConfig;
// app/routes.ts
import { type RouteConfig, index, route } from '@react-router/dev/routes';

export default [
  index('routes/home.tsx'),
  route('about', 'routes/about.tsx'),
] satisfies RouteConfig;

برای انجام این کار، تابع

route
route را از React Router ایمپورت کرده و مسیر
/about
/about را به آن معرفی می‌کنیم.

  1. آرگومان اول route، آدرس URL مورد نظر برای مطابقت است.
  2. آرگومان دوم، ماژول مسیری است که هنگام مطابقت URL نمایش داده می‌شود.

با انجام این پیکربندی، اکنون می‌توانیم به صفحه استاتیک About هدایت شویم.

ایجاد Build پروژه

برای build کردن پروژه و تولید صفحه About به صورت استاتیک، این دستور را در ترمینال اجرا می‌کنیم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
npm run build
npm run build
npm run build

پس از اجرای این دستور، یک فولدر جدید به نام

build/
build/ ایجاد می‌شود که شامل صفحه About به صورت استاتیک خواهد بود.

اما مسیرهای

/home
/home و
/about
/about تنها مسیرهای این پروژه نیستند؛ بنابراین، باید تمام مسیرهای اپلیکیشن را تنظیم کنیم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// app/routes.ts
import {
type RouteConfig,
index,
route,
layout,
} from '@react-router/dev/routes';
export default [
layout('routes/home.tsx', [
index('routes/book-list.tsx'),
route('book/:bookId', 'routes/book.tsx'),
]),
route('about', 'routes/about.tsx'),
] satisfies RouteConfig;
// app/routes.ts import { type RouteConfig, index, route, layout, } from '@react-router/dev/routes'; export default [ layout('routes/home.tsx', [ index('routes/book-list.tsx'), route('book/:bookId', 'routes/book.tsx'), ]), route('about', 'routes/about.tsx'), ] satisfies RouteConfig;
// app/routes.ts
import {
  type RouteConfig,
  index,
  route,
  layout,
} from '@react-router/dev/routes';

export default [
  layout('routes/home.tsx', [
    index('routes/book-list.tsx'),
    route('book/:bookId', 'routes/book.tsx'),
  ]),
  route('about', 'routes/about.tsx'),
] satisfies RouteConfig;

همان‌طور که در پیکربندی مسیرها مشاهده می‌کنیم، از یک تابع

layout
layout استفاده شده است که دو آرگومان دارد:

  1. مکان ماژول مسیر template
  2. یک آرایه از مسیرهای تودرتو

وقتی کاربر به یکی از مسیرهای تودرتو هدایت شود:

  1. ابتدا مسیر
    layout
    layout parent نمایش داده می‌شود.
  2. سپس React Router از کامپوننت
    <Outlet />
    <Outlet /> استفاده می‌کند تا محتوای مربوط به مسیر خاصی که کاربر به آن هدایت شده را نمایش دهد.

دریافت و بارگذاری داده‌ها در مسیرهای SSR

توابع loader مفهومی منحصربه‌فرد در React Router هستند. این توابع از ماژول‌های مسیر export می‌شوند و داده‌های لازم برای رندر شدن یک مسیر را return می‌کنند. این توابع فقط باید در ماژول‌های مسیر استفاده شوند و در جای دیگری نباید مورد استفاده قرار بگیرند.

در این اپلیکیشن، مسیری ایجاد می‌کنیم که تمام کتاب‌های دنبال‌شده توسط کاربر را نمایش دهد. این ماژول مسیر جدید از loaderها برای بارگذاری داده‌ها در زمان موردنیاز استفاده می‌کند (در این مثال، منظور ما داده‌های مربوط به کتاب‌های ذخیره شده می‌باشد). SSR در React Router 7 نیز از همین loaderها برای دریافت داده‌ها در سمت سرور استفاده می‌کند تا صفحات سریع‌تر و بهینه‌تر رندر شوند. برای این کار، ابتدا یک راه‌حل ذخیره‌سازی داده ایجاد می‌کنیم که در این مثال، یک آرایه جاوااسکریپت ساده برای نمایش داده‌ها است.

فایل

app/model.ts
app/model.ts را می‌سازیم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// app/model.ts
interface Book {
id: number;
title: string;
author: string;
isFinished: boolean;
isbn?: string;
rating?: 1 | 2 | 3 | 4 | 5;
}
interface Data {
books: Book[];
}
const storage: Data = {
books: [
{
id: 0,
title: `Numbers Don't Lie: 71 Stories to Help Us Understand the Modern World`,
author: 'Vaclav Smil',
isbn: `978-0241454411`,
isFinished: true,
rating: 1,
},
],
};
export { type Book, storage };
// app/model.ts interface Book { id: number; title: string; author: string; isFinished: boolean; isbn?: string; rating?: 1 | 2 | 3 | 4 | 5; } interface Data { books: Book[]; } const storage: Data = { books: [ { id: 0, title: `Numbers Don't Lie: 71 Stories to Help Us Understand the Modern World`, author: 'Vaclav Smil', isbn: `978-0241454411`, isFinished: true, rating: 1, }, ], }; export { type Book, storage };
// app/model.ts

interface Book {
  id: number;
  title: string;
  author: string;
  isFinished: boolean;
  isbn?: string;
  rating?: 1 | 2 | 3 | 4 | 5;
}

interface Data {
  books: Book[];
}

const storage: Data = {
  books: [
    {
      id: 0,
      title: `Numbers Don't Lie: 71 Stories to Help Us Understand the Modern World`,
      author: 'Vaclav Smil',
      isbn: `978-0241454411`,
      isFinished: true,
      rating: 1,
    },
  ],
};
export { type Book, storage };

لیست کتاب‌ها

در مرحله بعد، یک مسیر جدید می‌سازیم تا همه کتاب‌ها را از آبجکت

storage
storage نمایش دهد. برای این کار، یک ماژول مسیر به نام
book-list.tsx
book-list.tsx ایجاد می‌کنیم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// app/routes/book-list.tsx
import type { Route } from './+types/book-list';
import BookCard from '~/components/BookCard';
import { storage } from '~/model';
export async function loader({}: Route.LoaderArgs) {
return storage;
}
export default function BookList({ loaderData }: Route.ComponentProps) {
return (
<div className='mx-5'>
{loaderData.books
.slice()
.reverse()
.map((book) => (
<BookCard key={book.id} {...book} />
))}
</div>
);
}
// app/routes/book-list.tsx import type { Route } from './+types/book-list'; import BookCard from '~/components/BookCard'; import { storage } from '~/model'; export async function loader({}: Route.LoaderArgs) { return storage; } export default function BookList({ loaderData }: Route.ComponentProps) { return ( <div className='mx-5'> {loaderData.books .slice() .reverse() .map((book) => ( <BookCard key={book.id} {...book} /> ))} </div> ); }
// app/routes/book-list.tsx

import type { Route } from './+types/book-list';
import BookCard from '~/components/BookCard';
import { storage } from '~/model';

export async function loader({}: Route.LoaderArgs) {
  return storage;
}

export default function BookList({ loaderData }: Route.ComponentProps) {
  return (
    <div className='mx-5'>
      {loaderData.books
        .slice()
        .reverse()
        .map((book) => (
          <BookCard key={book.id} {...book} />
        ))}
    </div>
  );
}

همانطور که مشاهده می‌کنیم، این ماژول مسیر، یک تابع

loader
loader را export می‌کند. سپس کامپوننت اصلی، مسیر داده‌ای که تابع
loader
loader باز می‌گرداند را در
loaderData
loaderData دریافت می‌کند. اما قبل از اینکه خروجی این تغییرات را مشاهده کنیم، باید چند کار دیگر نیز انجام دهیم. برای این منظور، کامپوننت import شده
BookCard
BookCard که هنوز وجود ندارد را می‌سازیم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// app/components/BookCard.tsx
import { Link } from 'react-router';
import { IoCheckmarkCircle } from 'react-icons/io5';
import type { Book } from '~/model';
export default function BookCard({
id,
title,
author,
isFinished,
isbn,
rating,
}: Book) {
return (
<Link
to={`book/${id}`}
className='block flex px-5 py-4 max-w-lg mb-2.5 border border-black hover:shadow-md'
>
<div className='w-12 shrink-0'>
{isbn ? (
<img
className='w-full h-16'
src={`https://covers.openlibrary.org/b/isbn/${isbn}-S.jpg`}
alt={`Cover for ${title}`}
/>
) : (
<span className='w-full h-16 block bg-gray-200'></span>
)}
</div>
<div className='flex flex-col ml-4 grow'>
<span className='font-medium'>{title}</span>
<span>{author}</span>
<div className='flex justify-between'>
<span>Rating: {rating ? `${rating}/5` : 'None'}</span>
{isFinished && (
<span className='flex items-center gap-1'>
Finished <IoCheckmarkCircle className='text-green-600' />
</span>
)}
</div>
</div>
</Link>
);
}
// app/components/BookCard.tsx import { Link } from 'react-router'; import { IoCheckmarkCircle } from 'react-icons/io5'; import type { Book } from '~/model'; export default function BookCard({ id, title, author, isFinished, isbn, rating, }: Book) { return ( <Link to={`book/${id}`} className='block flex px-5 py-4 max-w-lg mb-2.5 border border-black hover:shadow-md' > <div className='w-12 shrink-0'> {isbn ? ( <img className='w-full h-16' src={`https://covers.openlibrary.org/b/isbn/${isbn}-S.jpg`} alt={`Cover for ${title}`} /> ) : ( <span className='w-full h-16 block bg-gray-200'></span> )} </div> <div className='flex flex-col ml-4 grow'> <span className='font-medium'>{title}</span> <span>{author}</span> <div className='flex justify-between'> <span>Rating: {rating ? `${rating}/5` : 'None'}</span> {isFinished && ( <span className='flex items-center gap-1'> Finished <IoCheckmarkCircle className='text-green-600' /> </span> )} </div> </div> </Link> ); }
// app/components/BookCard.tsx

import { Link } from 'react-router';
import { IoCheckmarkCircle } from 'react-icons/io5';
import type { Book } from '~/model';

export default function BookCard({
  id,
  title,
  author,
  isFinished,
  isbn,
  rating,
}: Book) {
  return (
    <Link
      to={`book/${id}`}
      className='block flex px-5 py-4 max-w-lg mb-2.5 border border-black hover:shadow-md'
    >
      <div className='w-12 shrink-0'>
        {isbn ? (
          <img
            className='w-full h-16'
            src={`https://covers.openlibrary.org/b/isbn/${isbn}-S.jpg`}
            alt={`Cover for ${title}`}
          />
        ) : (
          <span className='w-full h-16 block bg-gray-200'></span>
        )}
      </div>
      <div className='flex flex-col ml-4 grow'>
        <span className='font-medium'>{title}</span>
        <span>{author}</span>
        <div className='flex justify-between'>
          <span>Rating: {rating ? `${rating}/5` : 'None'}</span>
          {isFinished && (
            <span className='flex items-center gap-1'>
              Finished <IoCheckmarkCircle className='text-green-600' />
            </span>
          )}
        </div>
      </div>
    </Link>
  );
}

کامپوننت

<BookCard />
<BookCard /> یک کارت قابل کلیک است. این کارت شامل مهم‌ترین اطلاعات در مورد یک ورودی کتاب مانند عنوان، نویسنده و احتمالاً کاور می‌باشد و اطلاعات دیگری نیز ممکن است در آن گنجانده شده باشد.

پس از آن، فایل

app/routes.tsx
app/routes.tsx را باز کرده و مسیر دیگر را کامنت می‌کنیم. این کار باعث می‌شود که React Router خطا ندهد؛ زیرا، هنوز برای آن ماژول مسیری تعریف نکرده‌ایم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// app/routes.tsx
...
export default [
layout('routes/home.tsx', [
index('routes/book-list.tsx'),
// route('book/:bookId', 'routes/book.tsx'),
]),
route('about', 'routes/about.tsx'),
] satisfies RouteConfig;
// app/routes.tsx ... export default [ layout('routes/home.tsx', [ index('routes/book-list.tsx'), // route('book/:bookId', 'routes/book.tsx'), ]), route('about', 'routes/about.tsx'), ] satisfies RouteConfig;
// app/routes.tsx

...
export default [
  layout('routes/home.tsx', [
    index('routes/book-list.tsx'),
    // route('book/:bookId', 'routes/book.tsx'),
  ]),
  route('about', 'routes/about.tsx'),
] satisfies RouteConfig;

با انجام این کارها، ما باید یک صفحه اصلی داشته باشیم که داده‌ها را از

storage
storage در فایل
app/model.ts
app/model.ts می‌خواند. این بدان معنی است که هر کتابی که به
storage
storage اضافه می‌شود باید در مسیر
book-list.tsx
book-list.tsx نمایش داده شود.

صفحه جزئیات کتاب

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

/book/:bookId
/book/:bookId را از حالت کامنت خارج می‌کنیم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// app/routes.ts
...
export default [
layout('routes/home.tsx', [
index('routes/book-list.tsx'),
route('book/:bookId', 'routes/book.tsx'),
]),
route('about', 'routes/about.tsx'),
] satisfies RouteConfig;
// app/routes.ts ... export default [ layout('routes/home.tsx', [ index('routes/book-list.tsx'), route('book/:bookId', 'routes/book.tsx'), ]), route('about', 'routes/about.tsx'), ] satisfies RouteConfig;
// app/routes.ts

...
export default [
  layout('routes/home.tsx', [
    index('routes/book-list.tsx'),
    route('book/:bookId', 'routes/book.tsx'),
  ]),
  route('about', 'routes/about.tsx'),
] satisfies RouteConfig;

سپس، ماژول مسیر مرتبط را می‌سازیم. فایل آن

app/routes/book.tsx
app/routes/book.tsx خواهد بود و شامل loader است که جزئیات هر کتابی را که کاربر روی آن کلیک کرده است، return می‌کند:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// app/routes/book.tsx
import { useState, type ChangeEvent } from 'react';
import { Link, Form } from 'react-router';
import { IoArrowBackCircle, IoStarOutline, IoStar } from 'react-icons/io5';
import type { Route } from './+types/book';
import Button from '~/components/Button';
import { storage, type Book } from '~/model';
export async function loader({ params }: Route.LoaderArgs) {
const { bookId } = params;
const book: Book | undefined = storage.books.find(({ id }) => +bookId === id);
return book;
}
export default function Book({ loaderData }: Route.ComponentProps) {
const [isFinished, setIsFinished] = useState<boolean>(
loaderData?.isFinished || false
);
const [rating, setRating] = useState<number>(Number(loaderData?.rating));
return (
<div className='mx-5'>
<Link to='/' className='text-purple-700 flex items-center gap-1 w-fit'>
<IoArrowBackCircle /> Back to home
</Link>
<div className='flex mt-5 max-w-md'>
<div className='w-48 h-72 shrink-0'>
{loaderData?.isbn ? (
<img
className='w-full h-full'
src={`https://covers.openlibrary.org/b/isbn/${loaderData.isbn}-L.jpg`}
alt={`Cover for ${loaderData.title}`}
/>
) : (
<span className='block w-full h-full bg-gray-200'></span>
)}
</div>
<div className='flex flex-col ml-5 grow'>
<span className='font-medium text-xl'>{loaderData?.title}</span>
<span>{loaderData?.author}</span>
<Form method='post'>
<span className='my-5 block'>
<input
type='checkbox'
name='isFinished'
id='finished'
checked={isFinished}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setIsFinished(e.target.checked)
}
/>
<label htmlFor='finished' className='ml-2'>
Finished
</label>
</span>
<div className='mb-5'>
<span>Your Rating:</span>
<span className='text-3xl flex'>
{[۱, ۲, ۳, ۴, ۵].map((num) => {
return (
<span key={num} className='flex'>
<input
className='hidden'
type='radio'
name='rating'
id={`rating-${num}`}
value={num}
checked={rating === num}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setRating(+e.target.value)
}
/>
<label htmlFor={`rating-${num}`}>
{num <= rating ? <IoStar /> : <IoStarOutline />}
</label>
</span>
);
})}
</span>
</div>
<div className='text-right'>
<Button type='submit'>Save</Button>
<Button variant='delete' type='button'>
Delete Book
</Button>
</div>
</Form>
</div>
</div>
</div>
);
}
// app/routes/book.tsx import { useState, type ChangeEvent } from 'react'; import { Link, Form } from 'react-router'; import { IoArrowBackCircle, IoStarOutline, IoStar } from 'react-icons/io5'; import type { Route } from './+types/book'; import Button from '~/components/Button'; import { storage, type Book } from '~/model'; export async function loader({ params }: Route.LoaderArgs) { const { bookId } = params; const book: Book | undefined = storage.books.find(({ id }) => +bookId === id); return book; } export default function Book({ loaderData }: Route.ComponentProps) { const [isFinished, setIsFinished] = useState<boolean>( loaderData?.isFinished || false ); const [rating, setRating] = useState<number>(Number(loaderData?.rating)); return ( <div className='mx-5'> <Link to='/' className='text-purple-700 flex items-center gap-1 w-fit'> <IoArrowBackCircle /> Back to home </Link> <div className='flex mt-5 max-w-md'> <div className='w-48 h-72 shrink-0'> {loaderData?.isbn ? ( <img className='w-full h-full' src={`https://covers.openlibrary.org/b/isbn/${loaderData.isbn}-L.jpg`} alt={`Cover for ${loaderData.title}`} /> ) : ( <span className='block w-full h-full bg-gray-200'></span> )} </div> <div className='flex flex-col ml-5 grow'> <span className='font-medium text-xl'>{loaderData?.title}</span> <span>{loaderData?.author}</span> <Form method='post'> <span className='my-5 block'> <input type='checkbox' name='isFinished' id='finished' checked={isFinished} onChange={(e: ChangeEvent<HTMLInputElement>) => setIsFinished(e.target.checked) } /> <label htmlFor='finished' className='ml-2'> Finished </label> </span> <div className='mb-5'> <span>Your Rating:</span> <span className='text-3xl flex'> {[۱, ۲, ۳, ۴, ۵].map((num) => { return ( <span key={num} className='flex'> <input className='hidden' type='radio' name='rating' id={`rating-${num}`} value={num} checked={rating === num} onChange={(e: ChangeEvent<HTMLInputElement>) => setRating(+e.target.value) } /> <label htmlFor={`rating-${num}`}> {num <= rating ? <IoStar /> : <IoStarOutline />} </label> </span> ); })} </span> </div> <div className='text-right'> <Button type='submit'>Save</Button> <Button variant='delete' type='button'> Delete Book </Button> </div> </Form> </div> </div> </div> ); }
// app/routes/book.tsx

import { useState, type ChangeEvent } from 'react';
import { Link, Form } from 'react-router';
import { IoArrowBackCircle, IoStarOutline, IoStar } from 'react-icons/io5';
import type { Route } from './+types/book';
import Button from '~/components/Button';
import { storage, type Book } from '~/model';

export async function loader({ params }: Route.LoaderArgs) {
  const { bookId } = params;
  const book: Book | undefined = storage.books.find(({ id }) => +bookId === id);
  return book;
}

export default function Book({ loaderData }: Route.ComponentProps) {
  const [isFinished, setIsFinished] = useState<boolean>(
    loaderData?.isFinished || false
  );
  const [rating, setRating] = useState<number>(Number(loaderData?.rating));
  return (
    <div className='mx-5'>
      <Link to='/' className='text-purple-700 flex items-center gap-1 w-fit'>
        <IoArrowBackCircle /> Back to home
      </Link>
      <div className='flex mt-5 max-w-md'>
        <div className='w-48 h-72 shrink-0'>
          {loaderData?.isbn ? (
            <img
              className='w-full h-full'
              src={`https://covers.openlibrary.org/b/isbn/${loaderData.isbn}-L.jpg`}
              alt={`Cover for ${loaderData.title}`}
            />
          ) : (
            <span className='block w-full h-full bg-gray-200'></span>
          )}
        </div>
        <div className='flex flex-col ml-5 grow'>
          <span className='font-medium text-xl'>{loaderData?.title}</span>
          <span>{loaderData?.author}</span>
          <Form method='post'>
            <span className='my-5 block'>
              <input
                type='checkbox'
                name='isFinished'
                id='finished'
                checked={isFinished}
                onChange={(e: ChangeEvent<HTMLInputElement>) =>
                  setIsFinished(e.target.checked)
                }
              />
              <label htmlFor='finished' className='ml-2'>
                Finished
              </label>
            </span>
            <div className='mb-5'>
              <span>Your Rating:</span>
              <span className='text-3xl flex'>
                {[۱, ۲, ۳, ۴, ۵].map((num) => {
                  return (
                    <span key={num} className='flex'>
                      <input
                        className='hidden'
                        type='radio'
                        name='rating'
                        id={`rating-${num}`}
                        value={num}
                        checked={rating === num}
                        onChange={(e: ChangeEvent<HTMLInputElement>) =>
                          setRating(+e.target.value)
                        }
                      />
                      <label htmlFor={`rating-${num}`}>
                        {num <= rating ? <IoStar /> : <IoStarOutline />}
                      </label>
                    </span>
                  );
                })}
              </span>
            </div>
            <div className='text-right'>
              <Button type='submit'>Save</Button>
              <Button variant='delete' type='button'>
                Delete Book
              </Button>
            </div>
          </Form>
        </div>
      </div>
    </div>
  );
}

این فایل چندین عملکرد کلیدی دارد. ابتدا، بعد از importها، یک loader وجود دارد که آبجکت

storage
storage را جستجو می‌کند و آبجکت کتاب مربوط به شناسه موجود در پارامترهای URL را بازیابی می‌نماید. به عنوان مثال، اگر کاربر به
/book/0
/book/0 برود، loader جزئیات کتاب با شناسه
۰
۰ را بارگذاری خواهد کرد. علاوه بر این، ماژول مسیر به کاربران اجازه می‌دهد تا جزئیات کتاب را تغییر دهند. کاربران می‌توانند مشخص کنند که آیا کتاب را تمام کرده‌اند یا نه، امتیاز از پنج ستاره بدهند و تغییرات خود را ذخیره کنند. همچنین این امکان را دارند که کتاب را کاملاً حذف نمایند.

در ادامه قصد داریم تا به بررسی افزودن و حذف کتاب از book tracker بپردازیم.

سرور اکشن‌ها در React Router

مشابه loaderها، اکشن‌ها نیز فقط در ماژول‌های route اجرا می‌شوند. این ماژول‌ها همان فایل‌هایی هستند که در دایرکتوری

app/routes/
app/routes/ قرار دارند.

اکشن‌ها، توابعی هستند که وظیفه پردازش داده‌های ارسال‌شده از فرم‌ها را در یک مسیر مشخص بر عهده دارند:

  • اکشن‌هایی که در مرورگر اجرا می‌شوند با نام 
    clientAction
    clientAction export می‌شوند.
  • اکشن‌هایی که در سرور اجرا می‌شوند با نام 
    action
    action export می‌شوند.

ساختار کلی اکشن‌ها

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

  1. پارامترهای URL (به عنوان
    params
    params)
  2. داده‌های ارسال‌شده به مسیر (به عنوان
    request
    request)

پارامتر

request
request در اینجا نمونه‌ای از Request Web API است. این بدان معناست که می‌توانیم از تمام قابلیت‌های API در این درخواست استفاده کنیم.
همچنین این پارامترها از
Route.ActionArgs
Route.ActionArgs گرفته می‌شوند که هر ماژول مسیر، نسخه‌ای منحصربه‌فرد از آن را در
.react-router
.react-router دارد.

اضافه کردن یک کتاب جدید به ذخیره‌سازی با Server Actionها

اولین کاری که در این مقاله با Server Actionها انجام می‌دهیم، افزودن یک کتاب جدید به

storage
storage است. برای این کار، تابع
action
action زیر را به ماژول
book-list.tsx
book-list.tsx اضافه می‌کنیم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// app/routes/book-list.tsx
...
export async function action({ request }: Route.ActionArgs) {
let formData = await request.formData();
let title = formData.get('title') as string | null;
let author = formData.get('author') as string | null;
let isbn = formData.get('isbn') as string | undefined;
if (title && author) {
storage.books.push({
id: storage.books.length,
title,
author,
isbn: isbn || undefined,
isFinished: false,
});
}
return storage;
}
...
// app/routes/book-list.tsx ... export async function action({ request }: Route.ActionArgs) { let formData = await request.formData(); let title = formData.get('title') as string | null; let author = formData.get('author') as string | null; let isbn = formData.get('isbn') as string | undefined; if (title && author) { storage.books.push({ id: storage.books.length, title, author, isbn: isbn || undefined, isFinished: false, }); } return storage; } ...
// app/routes/book-list.tsx
...
export async function action({ request }: Route.ActionArgs) {
  let formData = await request.formData();
  let title = formData.get('title') as string | null;
  let author = formData.get('author') as string | null;
  let isbn = formData.get('isbn') as string | undefined;
  if (title && author) {
    storage.books.push({
      id: storage.books.length,
      title,
      author,
      isbn: isbn || undefined,
      isFinished: false,
    });
  }

  return storage;
}

...

پس از پیاده‌سازی این تابع، می‌توانیم کتاب‌های جدید را به برنامه اضافه نماییم. بعد از پر کردن فرم، کتاب جدید باید در مسیر

book-list.tsx
book-list.tsx ظاهر شود.

ویرایش و حذف کتاب با Server Actionها

بعد از اضافه کردن کتاب، حالا باید امکان ویرایش و حذف ورودی‌های کتاب را نیز فراهم کنیم.
برای این کار، یک اکشن در مسیر

book.tsx
book.tsx اضافه می‌کنیم.

این اکشن:

  • اطلاعات کتاب موردنظر را در
    storage
    storage به‌روزرسانی می‌کند.
  • در صورتی که متد درخواست برابر
    "DELETE"
    "DELETE" باشد، کتاب را حذف می‌کند:
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// app/routes/book.tsx
import { useState, type ChangeEvent } from 'react';
import { Link, Form, redirect, useSubmit } from 'react-router';
import { IoArrowBackCircle, IoStarOutline, IoStar } from 'react-icons/io5';
import type { Route } from './+types/book';
import Button from '~/components/Button';
import { storage, type Book } from '~/model';
export async function action({ params, request }: Route.ActionArgs) {
let formData = await request.formData();
let { bookId } = params;
let newRating = (Number(formData.get('rating')) ||
undefined) as Book['rating'];
let isFinished = Boolean(formData.get('isFinished'));
if (request.method === 'DELETE') {
storage.books = storage.books.filter(({ id }) => +bookId !== id);
} else if (newRating && storage.books[+bookId]) {
Object.assign(storage.books[+bookId], {
isFinished,
rating: newRating,
});
}
return redirect('/');
}
export async function loader({ params }: Route.LoaderArgs) {
const { bookId } = params;
const book: Book | undefined = storage.books.find(({ id }) => +bookId === id);
return book;
}
export default function Book({ loaderData }: Route.ComponentProps) {
const [isFinished, setIsFinished] = useState<boolean>(
loaderData?.isFinished || false
);
const [rating, setRating] = useState<number>(Number(loaderData?.rating));
const submit = useSubmit();
function deleteBook(bookId: number | undefined = loaderData?.id) {
const confirmation = confirm('Are you sure you want to delete this book?');
confirmation && bookId &&
submit(
{ id: bookId },
{
method: 'delete',
}
);
}
return (
<div className='mx-5'>
<Link to='/' className='text-purple-700 flex items-center gap-1 w-fit'>
<IoArrowBackCircle /> Back to home
</Link>
<div className='flex mt-5 max-w-md'>
<div className='w-48 h-72 shrink-0'>
{loaderData?.isbn ? (
<img
className='w-full h-full'
src={`https://covers.openlibrary.org/b/isbn/${loaderData.isbn}-L.jpg`}
alt={`Cover for ${loaderData.title}`}
/>
) : (
<span className='block w-full h-full bg-gray-200'></span>
)}
</div>
<div className='flex flex-col ml-5 grow'>
<span className='font-medium text-xl'>{loaderData?.title}</span>
<span>{loaderData?.author}</span>
<Form method='post'>
<span className='my-5 block'>
<input
type='checkbox'
name='isFinished'
id='finished'
checked={isFinished}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setIsFinished(e.target.checked)
}
/>
<label htmlFor='finished' className='ml-2'>
Finished
</label>
</span>
<div className='mb-5'>
<span>Your Rating:</span>
<span className='text-3xl flex'>
{[۱, ۲, ۳, ۴, ۵].map((num) => {
return (
<span key={num} className='flex'>
<input
className='hidden'
type='radio'
name='rating'
id={`rating-${num}`}
value={num}
checked={rating === num}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setRating(+e.target.value)
}
/>
<label htmlFor={`rating-${num}`}>
{num <= rating ? <IoStar /> : <IoStarOutline />}
</label>
</span>
);
})}
</span>
</div>
<div className='text-right'>
<Button type='submit'>Save</Button>
<Button
variant='delete'
type='button'
onClick={() => deleteBook()}
>
Delete Book
</Button>
</div>
</Form>
</div>
</div>
</div>
);
}
// app/routes/book.tsx import { useState, type ChangeEvent } from 'react'; import { Link, Form, redirect, useSubmit } from 'react-router'; import { IoArrowBackCircle, IoStarOutline, IoStar } from 'react-icons/io5'; import type { Route } from './+types/book'; import Button from '~/components/Button'; import { storage, type Book } from '~/model'; export async function action({ params, request }: Route.ActionArgs) { let formData = await request.formData(); let { bookId } = params; let newRating = (Number(formData.get('rating')) || undefined) as Book['rating']; let isFinished = Boolean(formData.get('isFinished')); if (request.method === 'DELETE') { storage.books = storage.books.filter(({ id }) => +bookId !== id); } else if (newRating && storage.books[+bookId]) { Object.assign(storage.books[+bookId], { isFinished, rating: newRating, }); } return redirect('/'); } export async function loader({ params }: Route.LoaderArgs) { const { bookId } = params; const book: Book | undefined = storage.books.find(({ id }) => +bookId === id); return book; } export default function Book({ loaderData }: Route.ComponentProps) { const [isFinished, setIsFinished] = useState<boolean>( loaderData?.isFinished || false ); const [rating, setRating] = useState<number>(Number(loaderData?.rating)); const submit = useSubmit(); function deleteBook(bookId: number | undefined = loaderData?.id) { const confirmation = confirm('Are you sure you want to delete this book?'); confirmation && bookId && submit( { id: bookId }, { method: 'delete', } ); } return ( <div className='mx-5'> <Link to='/' className='text-purple-700 flex items-center gap-1 w-fit'> <IoArrowBackCircle /> Back to home </Link> <div className='flex mt-5 max-w-md'> <div className='w-48 h-72 shrink-0'> {loaderData?.isbn ? ( <img className='w-full h-full' src={`https://covers.openlibrary.org/b/isbn/${loaderData.isbn}-L.jpg`} alt={`Cover for ${loaderData.title}`} /> ) : ( <span className='block w-full h-full bg-gray-200'></span> )} </div> <div className='flex flex-col ml-5 grow'> <span className='font-medium text-xl'>{loaderData?.title}</span> <span>{loaderData?.author}</span> <Form method='post'> <span className='my-5 block'> <input type='checkbox' name='isFinished' id='finished' checked={isFinished} onChange={(e: ChangeEvent<HTMLInputElement>) => setIsFinished(e.target.checked) } /> <label htmlFor='finished' className='ml-2'> Finished </label> </span> <div className='mb-5'> <span>Your Rating:</span> <span className='text-3xl flex'> {[۱, ۲, ۳, ۴, ۵].map((num) => { return ( <span key={num} className='flex'> <input className='hidden' type='radio' name='rating' id={`rating-${num}`} value={num} checked={rating === num} onChange={(e: ChangeEvent<HTMLInputElement>) => setRating(+e.target.value) } /> <label htmlFor={`rating-${num}`}> {num <= rating ? <IoStar /> : <IoStarOutline />} </label> </span> ); })} </span> </div> <div className='text-right'> <Button type='submit'>Save</Button> <Button variant='delete' type='button' onClick={() => deleteBook()} > Delete Book </Button> </div> </Form> </div> </div> </div> ); }
// app/routes/book.tsx

import { useState, type ChangeEvent } from 'react';
import { Link, Form, redirect, useSubmit } from 'react-router';
import { IoArrowBackCircle, IoStarOutline, IoStar } from 'react-icons/io5';
import type { Route } from './+types/book';
import Button from '~/components/Button';
import { storage, type Book } from '~/model';

export async function action({ params, request }: Route.ActionArgs) {
  let formData = await request.formData();
  let { bookId } = params;
  let newRating = (Number(formData.get('rating')) ||
    undefined) as Book['rating'];
  let isFinished = Boolean(formData.get('isFinished'));
  if (request.method === 'DELETE') {
    storage.books = storage.books.filter(({ id }) => +bookId !== id);
  } else if (newRating && storage.books[+bookId]) {
    Object.assign(storage.books[+bookId], {
      isFinished,
      rating: newRating,
    });
  }
  return redirect('/');
}

export async function loader({ params }: Route.LoaderArgs) {
  const { bookId } = params;
  const book: Book | undefined = storage.books.find(({ id }) => +bookId === id);
  return book;
}

export default function Book({ loaderData }: Route.ComponentProps) {
  const [isFinished, setIsFinished] = useState<boolean>(
    loaderData?.isFinished || false
  );
  const [rating, setRating] = useState<number>(Number(loaderData?.rating));

  const submit = useSubmit();

  function deleteBook(bookId: number | undefined = loaderData?.id) {
    const confirmation = confirm('Are you sure you want to delete this book?');
    confirmation && bookId &&
      submit(
        { id: bookId },
        {
          method: 'delete',
        }
      );
  }

  return (
    <div className='mx-5'>
      <Link to='/' className='text-purple-700 flex items-center gap-1 w-fit'>
        <IoArrowBackCircle /> Back to home
      </Link>
      <div className='flex mt-5 max-w-md'>
        <div className='w-48 h-72 shrink-0'>
          {loaderData?.isbn ? (
            <img
              className='w-full h-full'
              src={`https://covers.openlibrary.org/b/isbn/${loaderData.isbn}-L.jpg`}
              alt={`Cover for ${loaderData.title}`}
            />
          ) : (
            <span className='block w-full h-full bg-gray-200'></span>
          )}
        </div>
        <div className='flex flex-col ml-5 grow'>
          <span className='font-medium text-xl'>{loaderData?.title}</span>
          <span>{loaderData?.author}</span>
          <Form method='post'>
            <span className='my-5 block'>
              <input
                type='checkbox'
                name='isFinished'
                id='finished'
                checked={isFinished}
                onChange={(e: ChangeEvent<HTMLInputElement>) =>
                  setIsFinished(e.target.checked)
                }
              />
              <label htmlFor='finished' className='ml-2'>
                Finished
              </label>
            </span>
            <div className='mb-5'>
              <span>Your Rating:</span>
              <span className='text-3xl flex'>
                {[۱, ۲, ۳, ۴, ۵].map((num) => {
                  return (
                    <span key={num} className='flex'>
                      <input
                        className='hidden'
                        type='radio'
                        name='rating'
                        id={`rating-${num}`}
                        value={num}
                        checked={rating === num}
                        onChange={(e: ChangeEvent<HTMLInputElement>) =>
                          setRating(+e.target.value)
                        }
                      />
                      <label htmlFor={`rating-${num}`}>
                        {num <= rating ? <IoStar /> : <IoStarOutline />}
                      </label>
                    </span>
                  );
                })}
              </span>
            </div>
            <div className='text-right'>
              <Button type='submit'>Save</Button>
              <Button
                variant='delete'
                type='button'
                onClick={() => deleteBook()}
              >
                Delete Book
              </Button>
            </div>
          </Form>
        </div>
      </div>
    </div>
  );
}

اکنون، کاربران می‌توانند جزئیات مربوط به هر کتاب را ذخیره کنند و هر کتاب را از برنامه (یا

storage
storage) حذف نمایند.

مدیریت Status Codeها در React Router

status codeهای HTTP مشخص می‌کنند که وضعیت یک درخواست کلاینت به سرور چگونه است.
برخی از رایج‌ترین status codeها عبارتند از:

  • ۲۰۰
    ۲۰۰ درخواست موفق بوده است.
  • ۲۰۱
    ۲۰۱ درخواست موفق بوده و یک ورودی جدید ایجاد شده است.
  • ۴۰۴
    ۴۰۴ منبع درخواستی در سرور یافت نشد.
  • و غیره

مدیریت status codeها در React Router

به طور پیش‌فرض، هر صفحه‌ای که درخواست شود، یک status code

200
200 دریافت می‌کند (به معنای موفقیت‌آمیز بودن درخواست). اگر مسیر مورد نظر وجود نداشته باشد، کد
۴۰۴
۴۰۴ برگردانده می‌شود. اما React Router به توسعه‌دهندگان این امکان را می‌دهد که status codeهای سفارشی را ارسال کنند. SSR در React Router 7 نیز از این قابلیت برای کنترل بهتر پاسخ‌های سرور و ارائه تجربه کاربری بهینه‌تر بهره می‌برد. با این کار، API مربوط به کلاینت بهبود پیدا می‌کند و کاربران از وضعیت دقیق درخواست‌هایشان مطلع می‌شوند.

برای ارسال status code

201
201 هنگام ایجاد یک کتاب جدید، اپلیکیشن را به صورت زیر تغییر می‌دهیم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// app/routes/book-list.tsx
// Imports
import { data } from 'react-router';
...
export async function action({ request }: Route.ActionArgs) {
let formData = await request.formData();
let title = formData.get('title') as string | null;
let author = formData.get('author') as string | null;
let isbn = formData.get('isbn') as string | undefined;
if (title && author) {
storage.books.push({
id: storage.books.length,
title,
author,
isbn: isbn || undefined,
isFinished: false,
});
}
return data(storage, { status: 201 });
}
...
// app/routes/book-list.tsx // Imports import { data } from 'react-router'; ... export async function action({ request }: Route.ActionArgs) { let formData = await request.formData(); let title = formData.get('title') as string | null; let author = formData.get('author') as string | null; let isbn = formData.get('isbn') as string | undefined; if (title && author) { storage.books.push({ id: storage.books.length, title, author, isbn: isbn || undefined, isFinished: false, }); } return data(storage, { status: 201 }); } ...
// app/routes/book-list.tsx

// Imports
import { data } from 'react-router';
...

export async function action({ request }: Route.ActionArgs) {
  let formData = await request.formData();
  let title = formData.get('title') as string | null;
  let author = formData.get('author') as string | null;
  let isbn = formData.get('isbn') as string | undefined;
  if (title && author) {
    storage.books.push({
      id: storage.books.length,
      title,
      author,
      isbn: isbn || undefined,
      isFinished: false,
    });
  }
  return data(storage, { status: 201 });
}

...

سپس، زمانی که کاربر به یک مسیر

book/:bookId
book/:bookId نامعتبر هدایت می‌شود بایدکد
۴۰۴
۴۰۴ را return کنیم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// app/routes/book.tsx
// Imports
...
import { Link, Form, redirect, useSubmit, data } from 'react-router';
...
// Route module loader
export async function loader({ params }: Route.LoaderArgs) {
const { bookId } = params;
const book: Book | undefined = storage.books.find(({ id }) => +bookId === id);
if (!book) throw data(null, { status: 404 });
return book;
}
// app/routes/book.tsx // Imports ... import { Link, Form, redirect, useSubmit, data } from 'react-router'; ... // Route module loader export async function loader({ params }: Route.LoaderArgs) { const { bookId } = params; const book: Book | undefined = storage.books.find(({ id }) => +bookId === id); if (!book) throw data(null, { status: 404 }); return book; }
// app/routes/book.tsx

// Imports
...
import { Link, Form, redirect, useSubmit, data } from 'react-router';
...

// Route module loader
export async function loader({ params }: Route.LoaderArgs) {
  const { bookId } = params;
  const book: Book | undefined = storage.books.find(({ id }) => +bookId === id);

  if (!book) throw data(null, { status: 404 });

  return book;
}

اکنون می‌توانیم هر status code دیگری که مناسب است را در مسیرهای مختلف پروژه اضافه نماییم. با این کار، پروژه ما حرفه‌ای‌تر، ارتباطات API واضح‌تر و تجربه کاربری بهتر خواهد شد.

افزودن اطلاعات متا به تگ
<head>
<head>
 در React Router

تگ

<head>
<head> یکی از مهم‌ترین تگ‌ها برای سئو و بهینه‌سازی صفحات وب است.
فریم‌ورک React Router این امکان را فراهم می‌کند که تگ‌های
<meta>
<meta> را برای هر تعداد صفحه‌ای که بخواهیم، به‌روزرسانی نماییم.

تگ‌های

<meta>
<meta> شامل اطلاعات metadata می‌باشند، مانند:

  • title
  • description
  • keywords
  • viewport

اکنون می‌توانیم تگ‌های متا را به صفحات پروژه اضافه کنیم. برای انجام این کار، باید یک تابع

meta
meta در ماژول مسیر export نماییم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// app/routes/home.tsx
// Imports
...
import type { Route } from './+types/home';
...
export function meta({}: Route.MetaArgs) {
return [
{ title: 'Book Tracker App' },
{ name: 'description', content: 'Book Tracker Application' },
];
}
...
// app/routes/home.tsx // Imports ... import type { Route } from './+types/home'; ... export function meta({}: Route.MetaArgs) { return [ { title: 'Book Tracker App' }, { name: 'description', content: 'Book Tracker Application' }, ]; } ...
// app/routes/home.tsx

// Imports
...
import type { Route } from './+types/home';
...


export function meta({}: Route.MetaArgs) {
  return [
    { title: 'Book Tracker App' },
    { name: 'description', content: 'Book Tracker Application' },
  ];
}

...

همچنین، برای افزودن

<meta>
<meta> برای صفحه About:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// app/routes/about.tsx
// Imports
...
import type { Route } from './+types/book';
...
export function meta({}: Route.MetaArgs) {
return [
{ title: 'About Book Tracker App' },
{ name: 'description', content: 'About this Application' },
];
}
// app/routes/about.tsx // Imports ... import type { Route } from './+types/book'; ... export function meta({}: Route.MetaArgs) { return [ { title: 'About Book Tracker App' }, { name: 'description', content: 'About this Application' }, ]; }
// app/routes/about.tsx

// Imports
...
import type { Route } from './+types/book';
...

export function meta({}: Route.MetaArgs) {
  return [
    { title: 'About Book Tracker App' },
    { name: 'description', content: 'About this Application' },
  ];
}

در نهایت، برای افزودن

<meta>
<meta> برای مسیر
book.tsx
book.tsx:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// app/routes/book.tsx
// Imports
...
import type { Route } from './+types/book';
...
export function meta({ data }: Route.MetaArgs) {
return [{ title: `Edit "${data.title}"` }];
}
// app/routes/book.tsx // Imports ... import type { Route } from './+types/book'; ... export function meta({ data }: Route.MetaArgs) { return [{ title: `Edit "${data.title}"` }]; }
// app/routes/book.tsx

// Imports
...
import type { Route } from './+types/book';
...

export function meta({ data }: Route.MetaArgs) {
  return [{ title: `Edit "${data.title}"` }];
}

نکته‌ای که باید به آن توجه داشته باشیم این است که آرگومان

data
data در تابع
meta
meta همان مقداری است که loader این مسیر برمی‌گرداند و در متادیتا استفاده می‌شود.

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

افزودن linkهای HTML به تگ 
<head>
<head>
 در React Router

تگ

<link>
<link> برای تعریف ارتباط بین صفحه و منابع خارجی استفاده می‌شود. این تگ معمولا برای وارد کردن فایل‌های CSS، استفاده از Faviconهای و لینک دادن به فونت‌های خارجی مورد استفاده قرار می‌گیرد.

افزودن <link> در React Router

React Router به توسعه‌دهندگان اجازه می‌دهد که به صورت جداگانه، تگ‌های

<links>
<links> را به هر صفحه اضافه کنند. این قابلیت به ویژه در مواردی مثل استفاده از Favicon‌های سفارشی برای هر مسیر بسیار مفید می‌باشد. همچنین، SSR در React Router 7 این امکان را فراهم می‌کند که لینک‌های ضروری از پیش در سرور تعیین شوند تا هنگام بارگذاری صفحه، تجربه‌ای سریع‌تر و بهینه‌تر ارائه شود.

برای افزودن تگ <link>، در ماژول route یک تابع

links
links را export می‌کنیم:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
nlighter-language="typescript">export function links() {
return [
{
rel: 'icon',
href: '/favicon.png',
type: 'image/png',
},
];
}
nlighter-language="typescript">export function links() { return [ { rel: 'icon', href: '/favicon.png', type: 'image/png', }, ]; }
nlighter-language="typescript">export function links() {
  return [
    {
      rel: 'icon',
      href: '/favicon.png',
      type: 'image/png',
    },
  ];
}

درون این تابع، یک آرایه export می‌کنیم.  هر آیتم این آرایه یک آبجکت است که شامل ویژگی‌های یک

<link>
<link> در HTML می‌باشد. مقادیر این ویژگی‌ها برابر با مقدار attributeهای واقعی در یک تگ
<link>
<link> استاندارد HTML هستند.

با این روش، هر صفحه می‌تواند لینک‌های مخصوص خود را داشته باشد.

مدیریت هدرهای HTTP در React Router

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

برای اضافه کردن هدرهای سفارشی، می‌توانیم یک تابع

headers
headers را در ماژول route خود export نماییم. به عنوان مثال:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// Route module
export function headers(){
return {
"Content-Disposition": "inline",
...
"Header Name": "Header value"
}
}
// Route module export function headers(){ return { "Content-Disposition": "inline", ... "Header Name": "Header value" } }
// Route module

export function headers(){
  return {
    "Content-Disposition": "inline",
    ...
    "Header Name": "Header value"
  }
}

اکنون، کلاینت پاسخ را همراه با هدرهای سفارشی دریافت می‌کند.

جمع‌بندی

در این مقاله درباره SSR در React Router 7 صحبت کردیم که ترکیبی از React Router و Remix است و امکان ساخت اپلیکیشن‌های SSR و SSG را فراهم می‌کند.

برای درک بهتر این مفاهیم، یک اپلیکیشن مدیریت کتاب ایجاد کردیم و بهبودهای مهمی مثل type safety بهتر، تجربه توسعه راحت‌تر و قابلیت‌های جدید نسخه ۱۹ React را مورد بررسی قرار دادیم.

با استفاده از این راهنما، می‌توانیم SSR، SSG و ویژگی‌هایی مثل loaderها، Actionها و Meta Tagها را در پروژه‌های خود پیاده‌سازی نماییم.

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

دیدگاه‌ها:

افزودن دیدگاه جدید