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، نسخه ۲۰ Node.js است. بنابراین، باید مطمئن شویم که دستگاه ما از این نسخه یا بالاتر استفاده میکند:
node --version
سپس، فریمورک React Router را با اجرای دستور زیر نصب میکنیم:
npx create-react-router@latest react-router-ssr
در اینجا، نام پروژه نمونه ما
react-router-ssr
است. پس از اجرای دستور فوق، CLI از ما میپرسد که آیا میخواهیم یک git repo برای پروژه ایجاد کنیم یا خیر. در صورت تمایل، گزینه «yes» را انتخاب میکنیم. همچنین، از ما میپرسد که آیا میخواهیم وابستگیها را با استفاده از npm نصب نماییم یا نه. پس از انتخاب گزینههای مورد نظر، پوشهای با نام پروژه ما ایجاد میشود. به دایرکتوری پروژه میرویم و سرور توسعه برنامه را اجرا میکنیم:
cd react-router-ssr npm run dev
سپس، مرورگر خود را باز کرده و به آدرس
http://localhost:5173
میرویم. باید صفحه اصلی برنامه را مشاهده نماییم.
از آنجایی که این آموزش شامل استقرار برنامه با Docker نیست، میتوانیم تمام فایلهای مرتبط با Docker را برای داشتن یک کدبیس تمیزتر حذف کنیم. این فایلها شامل .
.dockerignore
، Dockerfile
، Dockerfile.bun
و Dockerfile.pnpm
هستند که در قالب پروژه برای مواردی که نیاز به استقرار با Docker است، گنجانده شدهاند.
به منظور استفاده از ۷ React Router برای SSR، باید مطمئن شویم که گزینه
ssr
در فایل پیکربندی React Router روی true
تنظیم شده باشد. به طور پیشفرض، این مقدار روی true
قرار دارد. فایل react-router.config.ts
را در ویرایشگر کد خود باز کرده و بررسی میکنیم:
//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
را باز کرده و تمام استایلهای مربوط به «dark mode» را غیرفعال میکنیم:
// 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/
تعریف میکنیم، اما home.tsx
به عنوان صفحه اصلی عمل خواهد کرد. همچنین، مسیرهای دیگری وجود خواهند داشت که از آن به عنوان layout استفاده میکنند. فایل app/routes/home.tsx
را ایجاد میکنیم.
در فایل
app/routes/home.tsx
، کامپوننت <Home />
را export میکنیم که شامل موارد زیر میباشد:
// 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 />
و <Footer />
) را که در ادامه ایجاد خواهیم کرد، و کامپوننت <Outlet />
از React Router را import میکند. <Outlet />
کامپوننتهای هر مسیر child را که از home.tsx
به عنوان layout استفاده میکند، رندر مینماید.
برای نمایش چیزی در صفحه، باید کامپوننتهای سفارشی import شده را ایجاد کنیم. با اصلاح پوشه
app/welcome
که همراه با layout است، شروع میکنیم:
app/welcome
را حذف کرده و یک پوشه جدید به نام app/components
میسازیم،welcome
را به components
تغییر داده و تمام فایلهای داخل آن را حذف مینماییم.سپس، در پوشه
app/components
، دو فایل جدید به نامهای Header.tsx
و Footer.tsx
ایجاد میکنیم.
کامپوننت
<Header />
یک <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
کامپوننت <Link />
را از React Router ایمپورت کرده است که در واقع یک تگ <a>
بهینه شده برای این فریمورک میباشد. همچنین، کامپوننت <BookForm />
را که هنوز وجود ندارد، import کردهایم. در نهایت، این فایل از استایلهای Tailwind CSS استفاده میکند تا المنتهای HTML در صفحه ظاهر مناسبی داشته باشند.
<BookForm />
اکنون باید کامپوننت
<BookForm />
را ایجاد کنیم. اما قبل از آن، لازم است که کامپوننت headless dialog از Radix را نصب کنیم. از این کامپوننت برای ساخت یک فرم دیالوگ جهت اضافه کردن کتاب جدید به سیستم ردیابی استفاده خواهیم کرد. همچنین، React Icons را نصب میکنیم، چون در برخی از کامپوننتهای آینده به آن نیاز داریم.
npm install @radix-ui/react-dialog react-icons
بعد از نصب این پکیجها، یک فایل جدید در مسیر
app/components
با نام BookForm.tsx
میسازیم و کد زیر را در آن قرار میدهیم:
// 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
از هوک useState
برای کنترل نمایش و بسته شدن دیالوگ استفاده میکند. همچنین، از Tailwind CSS برای استایلدهی و از React Icons برای نمایش آیکون (+) بهره میبرد.
<Button />
در بخش قبل در کد
BookForm.tsx
، ما یک کامپوننت <Button />
را import کردهایم، اما هنوز آن را نساختهایم. اکنون باید این کامپوننت را در مسیر app/components/Button.tsx
بسازیم.
کد کامپوننت
<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 />
یک دکمه با قابلیت استفاده مجدد را فراهم میکند که دارای سه نوع مختلف (normal
، cancel
و delete
) است. هر کدام از این حالتها دارای استایل خاص خود هستند.
<Footer />
در نهایت، برای تکمیل ساختار اولیه برنامه، باید کامپوننت
<Footer />
را برای مسیر home.tsx
ایجاد کنیم.
کد کامپوننت
<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 ساده ایجاد میکند که شامل متن کپیرایت میباشد.
SSR را میتوانیم به دو تکنیک کلی تقسیم کنیم:
در SSR داینامیک، سرور هنگام درخواست، صفحه را ایجاد کرده و سپس HTML را به سمت کلاینت (مرورگر) ارسال میکند تا در آنجا hydrate شود.
اما در SSG، تمامی فایلهای موردنیاز یک صفحه (HTML، CSS، JavaScript) در زمان build ایجاد میشوند. به همین دلیل، هنگام درخواست، سریعتر به کلاینت ارسال میگردند، زیرا نیازی به پردازش اضافی روی سرور ندارند.
هر دو روش مزایا و معایب خاص خود را دارند.
SSG مناسب است اگر:
SSR داینامیک مناسب است اگر:
به عنوان مثال، در این پروژهای که داریم مسیر
/about
را به صورت SSG ایجاد میکنیم.برای این کار، باید پیکربندی React Router را ویرایش کرده و مشخص کنیم که مسیر
/about
باید به صورت استاتیک (pre-rendered) ایجاد شود. در این صورت، برنامه فقط مسیر (یا صفحه) /about
را از قبل رندر میکند:
// 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;
در مسیر
app/routes/about.tsx
فایل جدید را ایجاد میکنیم. این فایل حاوی محتوای استاتیک مربوط به صفحه «درباره ما» خواهد بود:
// 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 را بررسی خواهیم کرد.
قبل از اینکه بتوانیم صفحه About را در مرورگر مشاهده کنیم، باید React Router را تنظیم نماییم تا هنگام پیمایش به مسیر
/about
، ماژول مسیر مربوطه (about.tsx
) را نمایش دهد.
این پیکربندی در فایل
app/routes.ts
انجام میشود، جایی که تمام سلسلهمراتب مسیرهای برنامه تعریف شده است:
// 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
را از React Router ایمپورت کرده و مسیر /about
را به آن معرفی میکنیم.
با انجام این پیکربندی، اکنون میتوانیم به صفحه استاتیک About هدایت شویم.
برای build کردن پروژه و تولید صفحه About به صورت استاتیک، این دستور را در ترمینال اجرا میکنیم:
npm run build
پس از اجرای این دستور، یک فولدر جدید به نام
build/
ایجاد میشود که شامل صفحه About به صورت استاتیک خواهد بود.
اما مسیرهای
/home
و /about
تنها مسیرهای این پروژه نیستند؛ بنابراین، باید تمام مسیرهای اپلیکیشن را تنظیم کنیم:
// 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
parent نمایش داده میشود.<Outlet />
استفاده میکند تا محتوای مربوط به مسیر خاصی که کاربر به آن هدایت شده را نمایش دهد.توابع loader مفهومی منحصربهفرد در React Router هستند. این توابع از ماژولهای مسیر export میشوند و دادههای لازم برای رندر شدن یک مسیر را return میکنند. این توابع فقط باید در ماژولهای مسیر استفاده شوند و در جای دیگری نباید مورد استفاده قرار بگیرند.
در این اپلیکیشن، مسیری ایجاد میکنیم که تمام کتابهای دنبالشده توسط کاربر را نمایش دهد. این ماژول مسیر جدید از loaderها برای بارگذاری دادهها در زمان موردنیاز استفاده میکند (در این مثال، منظور ما دادههای مربوط به کتابهای ذخیره شده میباشد). SSR در React Router 7 نیز از همین loaderها برای دریافت دادهها در سمت سرور استفاده میکند تا صفحات سریعتر و بهینهتر رندر شوند. برای این کار، ابتدا یک راهحل ذخیرهسازی داده ایجاد میکنیم که در این مثال، یک آرایه جاوااسکریپت ساده برای نمایش دادهها است.
فایل
app/model.ts
را میسازیم:
// 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
نمایش دهد. برای این کار، یک ماژول مسیر به نام book-list.tsx
ایجاد میکنیم:
// 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
را export میکند. سپس کامپوننت اصلی، مسیر دادهای که تابع loader
باز میگرداند را در loaderData
دریافت میکند. اما قبل از اینکه خروجی این تغییرات را مشاهده کنیم، باید چند کار دیگر نیز انجام دهیم. برای این منظور، کامپوننت import شده BookCard
که هنوز وجود ندارد را میسازیم:
// 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 />
یک کارت قابل کلیک است. این کارت شامل مهمترین اطلاعات در مورد یک ورودی کتاب مانند عنوان، نویسنده و احتمالاً کاور میباشد و اطلاعات دیگری نیز ممکن است در آن گنجانده شده باشد.
پس از آن، فایل
app/routes.tsx
را باز کرده و مسیر دیگر را کامنت میکنیم. این کار باعث میشود که React Router خطا ندهد؛ زیرا، هنوز برای آن ماژول مسیری تعریف نکردهایم:
// 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
در فایل app/model.ts
میخواند. این بدان معنی است که هر کتابی که به storage
اضافه میشود باید در مسیر book-list.tsx
نمایش داده شود.
هر زمان که یک کاربر روی کارت کتاب کلیک میکند، اپلیکیشن باید به صفحه جدیدی هدایت شود که جزئیات آن کتاب را نمایش دهد. برای تنظیم این ویژگی، ابتدا مسیر مربوط به صفحه
/book/:bookId
را از حالت کامنت خارج میکنیم:
// 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
خواهد بود و شامل loader است که جزئیات هر کتابی را که کاربر روی آن کلیک کرده است، return میکند:
// 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
را جستجو میکند و آبجکت کتاب مربوط به شناسه موجود در پارامترهای URL را بازیابی مینماید. به عنوان مثال، اگر کاربر به /book/0
برود، loader جزئیات کتاب با شناسه ۰
را بارگذاری خواهد کرد. علاوه بر این، ماژول مسیر به کاربران اجازه میدهد تا جزئیات کتاب را تغییر دهند. کاربران میتوانند مشخص کنند که آیا کتاب را تمام کردهاند یا نه، امتیاز از پنج ستاره بدهند و تغییرات خود را ذخیره کنند. همچنین این امکان را دارند که کتاب را کاملاً حذف نمایند.
در ادامه قصد داریم تا به بررسی افزودن و حذف کتاب از book tracker بپردازیم.
مشابه loaderها، اکشنها نیز فقط در ماژولهای route اجرا میشوند. این ماژولها همان فایلهایی هستند که در دایرکتوری
app/routes/
قرار دارند.
اکشنها، توابعی هستند که وظیفه پردازش دادههای ارسالشده از فرمها را در یک مسیر مشخص بر عهده دارند:
clientAction
export میشوند.action
export میشوند.اکشنها پارامترهایی را دریافت میکنند که شامل موارد زیر هستند:
params
)request
)پارامتر
request
در اینجا نمونهای از Request Web API است. این بدان معناست که میتوانیم از تمام قابلیتهای API در این درخواست استفاده کنیم.Route.ActionArgs
گرفته میشوند که هر ماژول مسیر، نسخهای منحصربهفرد از آن را در .react-router
دارد.
اولین کاری که در این مقاله با Server Actionها انجام میدهیم، افزودن یک کتاب جدید به
storage
است. برای این کار، تابع action
زیر را به ماژول book-list.tsx
اضافه میکنیم:
// 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.tsx
اضافه میکنیم.
این اکشن:
storage
بهروزرسانی میکند."DELETE"
باشد، کتاب را حذف میکند:// 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
) حذف نمایند.
status codeهای HTTP مشخص میکنند که وضعیت یک درخواست کلاینت به سرور چگونه است.
برخی از رایجترین status codeها عبارتند از:
۲۰۰
درخواست موفق بوده است.۲۰۱
درخواست موفق بوده و یک ورودی جدید ایجاد شده است.۴۰۴
منبع درخواستی در سرور یافت نشد.به طور پیشفرض، هر صفحهای که درخواست شود، یک status code
200
دریافت میکند (به معنای موفقیتآمیز بودن درخواست). اگر مسیر مورد نظر وجود نداشته باشد، کد ۴۰۴
برگردانده میشود. اما React Router به توسعهدهندگان این امکان را میدهد که status codeهای سفارشی را ارسال کنند. SSR در React Router 7 نیز از این قابلیت برای کنترل بهتر پاسخهای سرور و ارائه تجربه کاربری بهینهتر بهره میبرد. با این کار، API مربوط به کلاینت بهبود پیدا میکند و کاربران از وضعیت دقیق درخواستهایشان مطلع میشوند.
برای ارسال status code
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
نامعتبر هدایت میشود بایدکد ۴۰۴
را return کنیم:
// 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>
در React Routerتگ
<head>
یکی از مهمترین تگها برای سئو و بهینهسازی صفحات وب است.<meta>
را برای هر تعداد صفحهای که بخواهیم، بهروزرسانی نماییم.
تگهای
<meta>
شامل اطلاعات metadata میباشند، مانند:
اکنون میتوانیم تگهای متا را به صفحات پروژه اضافه کنیم. برای انجام این کار، باید یک تابع
meta
در ماژول مسیر export نماییم:
// 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>
برای صفحه About:
// 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>
برای مسیر book.tsx
:
// app/routes/book.tsx // Imports ... import type { Route } from './+types/book'; ... export function meta({ data }: Route.MetaArgs) { return [{ title: `Edit "${data.title}"` }]; }
نکتهای که باید به آن توجه داشته باشیم این است که آرگومان
data
در تابع meta
همان مقداری است که loader این مسیر برمیگرداند و در متادیتا استفاده میشود.
با این تغییرات، اطلاعات متای صفحه در تب مرورگر بهروزرسانی خواهد شد.
<head>
در React Routerتگ
<link>
برای تعریف ارتباط بین صفحه و منابع خارجی استفاده میشود. این تگ معمولا برای وارد کردن فایلهای CSS، استفاده از Faviconهای و لینک دادن به فونتهای خارجی مورد استفاده قرار میگیرد.
React Router به توسعهدهندگان اجازه میدهد که به صورت جداگانه، تگهای
<links>
را به هر صفحه اضافه کنند. این قابلیت به ویژه در مواردی مثل استفاده از Faviconهای سفارشی برای هر مسیر بسیار مفید میباشد. همچنین، SSR در React Router 7 این امکان را فراهم میکند که لینکهای ضروری از پیش در سرور تعیین شوند تا هنگام بارگذاری صفحه، تجربهای سریعتر و بهینهتر ارائه شود.
برای افزودن تگ <link>، در ماژول route یک تابع
links
را export میکنیم:
nlighter-language="typescript">export function links() { return [ { rel: 'icon', href: '/favicon.png', type: 'image/png', }, ]; }
درون این تابع، یک آرایه export میکنیم. هر آیتم این آرایه یک آبجکت است که شامل ویژگیهای یک
<link>
در HTML میباشد. مقادیر این ویژگیها برابر با مقدار attributeهای واقعی در یک تگ <link>
استاندارد HTML هستند.
با این روش، هر صفحه میتواند لینکهای مخصوص خود را داشته باشد.
هدرهای HTTP در React Router به سرور اجازه میدهند که همراه با محتوای درخواستی، اطلاعات بیشتری را به کلاینت ارسال کند.
این هدرها برای ارسال کوکیها به مرورگر، تنظیم Caching و بسیاری از موارد دیگر مورد استفاده قرار میگیرند.
برای اضافه کردن هدرهای سفارشی، میتوانیم یک تابع
headers
را در ماژول route خود export نماییم. به عنوان مثال:
// 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ها را در پروژههای خود پیادهسازی نماییم.
همچنین، دسترسی به کد نهایی پروژه از طریق این لینک امکانپذیر میباشد.
۵۰ درصد تخفیف ویژه نوروز فرانت کست تا پایان هفته
کد تخفیف: spr
دیدگاهها: