<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"><channel><title>نوشته های مصطفی رستگار</title><link>https://virgool.ir/feed/@m.rastegar991</link><description>برنامه نویس کامپیوتر و ازین جور مسخره بازی ها!</description><language>fa</language><pubDate>2026-11-10 20:20:40</pubDate><image><url>https://files.virgool.ir/upload/users/234580/avatar/JpSpbd.jpg</url><title>مصطفی رستگار</title><link>https://virgool.ir/@m.rastegar991</link></image><item><title>06- Mocking در تست‌ها (ماژول‌ها، API calls، توابع تاریخ/زمان)Mocking در تست‌ها (ماژول‌ها، API calls، توابع تاریخ/زمان)</title><link>https://virgool.ir/@m.rastegar991/06-mocking-در-تست-ها-ماژول-ها-api-calls-توابع-تاریخزمانmocking-در-تست-ها-ماژول-ها-api-calls-توابع-تاریخزمان-eu3zteen50kq</link><description>پاسخ به سوال Mocking در تست‌هاسلام. بگذارید این موضوع را با یک مثال ساده و قابل لمس توضیح بدهم. فرض کنید می‌خواهیم کیفیت یک خودرو را تست کنیم. برای تست موتور، نیازی نداریم کل خودرو را در جاده ببریم. می‌توانیم موتور را روی یک پایه قرار دهیم و آن را به صورت جداگانه تست کنیم. در دنیای نرم‌افزار، به این کار می‌گوییم Mocking.تعریف ساده و مفهومیMocking یعنی جایگزین کردن یک بخش واقعی از کد با یک نسخه ساختگی و کنترل شده در زمان تست. این کار را انجام می‌دهیم تا تست‌هایمان:وابسته به منابع خارجی نباشند (مثل اینترنت، دیتابیس، یا ساعت سیستم)سریع اجرا شوندقابل پیش‌بینی و تکرارپذیر باشندفقط منطق خودمان را تست کنند، نه منطق کتابخانه‌های دیگر راسناریوی اول: Mocking ماژول‌هافرض کنید یک تابع داریم که از یک ماژول دیگر برای ارسال ایمیل استفاده می‌کند:javascript// services/emailService.js&#xA;import { sendEmail } from &#39;./emailProvider&#39;;&#xA;&#xA;export function notifyUser(email) {&#xA;  const result = sendEmail(email, &#39;خوش آمدید&#39;);&#xA;  return result.success;&#xA;}در زمان تست، ما نمی‌خواهیم واقعا ایمیل بفرستیم. پس ماژول emailProvider را به صورت ساختگی در می‌آوریم:javascript// tests/emailService.test.js&#xA;import { notifyUser } from &#39;./emailService&#39;;&#xA;import * as emailProvider from &#39;./emailProvider&#39;;&#xA;&#xA;// این کار باعث می‌شود که تابع sendEmail یک نسخه ساختگی بگیرد&#xA;jest.mock(&#39;./emailProvider&#39;);&#xA;&#xA;test(&#39;وقتی ایمیل با موفقیت ارسال شود، true برمی‌گرداند&#39;, () =&gt; {&#xA;  // نسخه ساختگی را طوری تنظیم می‌کنیم که همیشه موفق باشد&#xA;  emailProvider.sendEmail.mockReturnValue({ success: true });&#xA;  &#xA;  const result = notifyUser(&#39;test@example.com&#39;);&#xA;  expect(result).toBe(true);&#xA;});&#xA;&#xA;test(&#39;وقتی ایمیل با خطا مواجه شود، false برمی‌گرداند&#39;, () =&gt; {&#xA;  emailProvider.sendEmail.mockReturnValue({ success: false });&#xA;  &#xA;  const result = notifyUser(&#39;test@example.com&#39;);&#xA;  expect(result).toBe(false);&#xA;});نکته کلیدی این است که ما واقعا کاری به نحوه کار sendEmail نداریم. فقط می‌خواهیم بدانیم که تابع خودمان در شرایط مختلف چه رفتاری از خود نشان می‌دهد.سناریوی دوم: Mocking درخواست‌های شبکه (API Calls)این مورد در برنامه‌های React و Next.js بسیار رایج است. فرض کنید یک کامپوننت داریم که اطلاعات کاربر را از سرور می‌گیرد:javascript// components/UserProfile.jsx&#xA;import { useEffect, useState } from &#39;react&#39;;&#xA;import axios from &#39;axios&#39;;&#xA;&#xA;export function UserProfile({ userId }) {&#xA;  const [user, setUser] = useState(null);&#xA;  const [loading, setLoading] = useState(true);&#xA;&#xA;  useEffect(() =&gt; {&#xA;    axios.get(`/api/users/${userId}`)&#xA;      .then(response =&gt; {&#xA;        setUser(response.data);&#xA;        setLoading(false);&#xA;      });&#xA;  }, [userId]);&#xA;&#xA;  if (loading) return &lt;div&gt;در حال بارگذاری...&lt;/div&gt;;&#xA;  return &lt;div&gt;{user?.name}&lt;/div&gt;;&#xA;}برای تست این کامپوننت، هرگز نباید به سرور واقعی متصل شویم. به جای آن، axios را با یک نسخه ساختگی جایگزین می‌کنیم:javascript// tests/UserProfile.test.jsx&#xA;import { render, screen } from &#39;@testing-library/react&#39;;&#xA;import axios from &#39;axios&#39;;&#xA;import { UserProfile } from &#39;./UserProfile&#39;;&#xA;&#xA;// کل کتابخانه axios را با نسخه ساختگی جایگزین می‌کنیم&#xA;jest.mock(&#39;axios&#39;);&#xA;&#xA;test(&#39;پس از دریافت اطلاعات، نام کاربر را نشان می‌دهد&#39;, async () =&gt; {&#xA;  // تنظیم پاسخ ساختگی برای درخواست GET&#xA;  const mockUser = { id: 1, name: &#39;علی رضایی&#39; };&#xA;  axios.get.mockResolvedValue({ data: mockUser });&#xA;&#xA;  render(&lt;UserProfile userId={1} /&gt;);&#xA;  &#xA;  // ابتدا باید متن بارگذاری را ببینیم&#xA;  expect(screen.getByText(&#39;در حال بارگذاری...&#39;)).toBeInTheDocument();&#xA;  &#xA;  // سپس منتظر می‌مانیم تا نام کاربر ظاهر شود&#xA;  const userName = await screen.findByText(&#39;علی رضایی&#39;);&#xA;  expect(userName).toBeInTheDocument();&#xA;  &#xA;  // اطمینان از اینکه درخواست با آدرس درست زده شده&#xA;  expect(axios.get).toHaveBeenCalledWith(&#39;/api/users/1&#39;);&#xA;});سناریوی سوم: Mocking توابع تاریخ و زماناین مورد یکی از چالش‌برانگیزترین موارد است چون تاریخ و زمان همیشه در حال تغییر هستند. فرض کنید یک تابع داریم که تخفیف ویژه را بر اساس روز هفته محاسبه می‌کند:javascript// utils/discount.js&#xA;export function getSpecialDiscount() {&#xA;  const today = new Date();&#xA;  const day = today.getDay(); // 0 = یکشنبه, 6 = شنبه&#xA;  &#xA;  if (day === 5) { // جمعه&#xA;    return 0.3; // تخفیف ۳۰ درصدی&#xA;  }&#xA;  return 0.1; // تخفیف معمولی ۱۰ درصدی&#xA;}برای تست این تابع، باید بتوانیم تاریخ را کنترل کنیم. در جاوااسکریپت، این کار را با جایگزین کردن Date اصلی انجام می‌دهیم:javascript// tests/discount.test.js&#xA;import { getSpecialDiscount } from &#39;./discount&#39;;&#xA;&#xA;// قبل از همه تست‌ها، یک تاریخ ثابت تنظیم می‌کنیم&#xA;beforeAll(() =&gt; {&#xA;  // تاریخ را روی یک جمعه خاص تنظیم می‌کنیم&#xA;  jest.useFakeTimers();&#xA;  jest.setSystemTime(new Date(&#39;2026-08-15&#39;)); // این تاریخ یک شنبه است&#xA;});&#xA;&#xA;afterAll(() =&gt; {&#xA;  // بعد از تست‌ها، حالت عادی را برمی‌گردانیم&#xA;  jest.useRealTimers();&#xA;});&#xA;&#xA;test(&#39;در روزهای جمعه تخفیف ویژه اعمال می‌شود&#39;, () =&gt; {&#xA;  // برای این تست، تاریخ را روی جمعه تنظیم می‌کنیم&#xA;  jest.setSystemTime(new Date(&#39;2026-08-14&#39;)); // این تاریخ جمعه است&#xA;  &#xA;  const discount = getSpecialDiscount();&#xA;  expect(discount).toBe(0.3);&#xA;});&#xA;&#xA;test(&#39;در روزهای غیر جمعه تخفیف معمولی است&#39;, () =&gt; {&#xA;  jest.setSystemTime(new Date(&#39;2026-08-15&#39;)); // شنبه&#xA;  &#xA;  const discount = getSpecialDiscount();&#xA;  expect(discount).toBe(0.1);&#xA;});نکات کلیدی و تفاوت سطح Seniorیک توسعه‌دهنده ارشد در بحث Mocking به این موارد توجه می‌کند:اول: همیشه از تست‌های واقعی در کنار تست‌های ساختگی استفاده می‌کند. مثلا یک تست مجزا برای اتصال واقعی به API دارد که هر شب اجرا می‌شود.دوم: زیاد Mock نمی‌کند. اگر چیزی خیلی ساده است، نیازی به ساختگی کردن ندارد.سوم: کد را طوری می‌نویسد که Mock کردن آسان باشد. مثلا وابستگی‌ها را به جای اینکه داخل تابع بسازد، به عنوان پارامتر یا از طریق تزریق وابستگی دریافت می‌کند.چهارم: در Next.js با App Router، برای تست کردن Server Components از mock کردن ماژول‌های سروری استفاده می‌کند و برای Client Components از mock کردن fetch یا axios.پنجم: همیشه بعد از هر تست، mockها را پاک می‌کند تا تست بعدی تحت تاثیر قرار نگیرد (jest.clearAllMocks()).در پاسخ به مصاحبه‌کننده، این را هم اضافه کنید که هدف از Mocking، تست کردن منطق کسب و کار است، نه تست کردن کتابخانه‌های شخص ثالث یا سرویس‌های خارجی. اگر مجبور هستید خیلی از چیزها را Mock کنید، احتمالا طراحی کد شما مشکل دارد و باید وابستگی‌ها را کاهش دهید.</description><category>مصطفی رستگار</category><author>مصطفی رستگار</author><pubDate>Sat, 15 Aug 2026 13:28:21 +0330</pubDate></item><item><title>05- Testing (Unit با Jest/Vitest، Integration با RTL، E2E با Playwright)</title><link>https://virgool.ir/@m.rastegar991/05-testing-unit-با-jestvitest-integration-با-rtl-e2e-با-playwright-mc83kxbtyaiz</link><description>پاسخ به سوال تست‌نویسی (Unit, Integration, E2E)برای شروع، اجازه دهید یک دید کلی به شما بدهم. در پروژه‌های React و Next.js، ما تست‌نویسی را به سه لایه اصلی تقسیم می‌کنیم. هر لایه هدف مشخصی دارد و جایگزین لایه دیگر نمی‌شود. به این سه لایه مثل یک هرم نگاه کنید که پهنای آن در پایین بیشتر است.لایه اول: تست واحد (Unit Test) با Jest یا Vitestتست واحد، کوچکترین بخش تست‌نویسی است. در اینجا ما یک تابع، یک هوک سفارشی، یا یک کامپوننت کوچک را به صورت کاملا ایزوله تست می‌کنیم. یعنی هیچ وابستگی به شبکه، دیتابیس، یا کامپوننت‌های دیگر نداریم.من در پروژه‌های جدید از Vitest استفاده می‌کنم چون سرعت بیشتری دارد و با Vite هماهنگ است. اما مفهوم هر دو یکی است.مثال عملی: فرض کنید یک تابع داریم که قیمت را با تخفیف محاسبه می‌کند. من برای این تابع سه سناریو می‌نویسم: حالت عادی، حالت با تخفیف صفر، و حالت با اعداد منفی که باید خطا بدهد.javascript// مثال ساده از تست واحد&#xA;test(&#39;should calculate discount price correctly&#39;, () =&gt; {&#xA;  expect(calculateDiscount(100, 10)).toBe(90);&#xA;  expect(calculateDiscount(100, 0)).toBe(100);&#xA;  expect(() =&gt; calculateDiscount(-10, 10)).toThrow();&#xA;});نکته مهم این است که در تست واحد، من هیچ وقت رندر واقعی انجام نمی‌دهم. فقط منطق خالص را تست می‌کنم. این تست‌ها باید خیلی سریع اجرا شوند، معمولا کمتر از چند میلی ثانیه برای هر تست.لایه دوم: تست یکپارچه‌سازی (Integration Test) با React Testing Libraryاینجا جایی است که من به عنوان یک توسعه‌دهنده ارشد، بیشترین زمان را صرف می‌کنم. تست یکپارچه‌سازی به معنای تست کردن تعامل چند بخش با هم است. مثلا یک کامپوننت فرم که با یک هوک سفارشی و یک سرویس API کار می‌کند.با React Testing Library، ما از دید کاربر تست می‌کنیم. یعنی دنبال متن‌ها، دکمه‌ها، و المان‌های قابل مشاهده می‌گردیم و کلیک کردن و تایپ کردن را شبیه‌سازی می‌کنیم.مثال عملی: یک فرم ورود کاربر را در نظر بگیرید. من در تست این فرم، سناریوی موفقیت و خطا را هر دو تست می‌کنم.javascript// مثال از تست یکپارچه‌سازی&#xA;test(&#39;should show error message when login fails&#39;, async () =&gt; {&#xA;  // شبیه‌سازی درخواست ناموفق&#xA;  server.use(&#xA;    rest.post(&#39;/api/login&#39;, (req, res, ctx) =&gt; {&#xA;      return res(ctx.status(401), ctx.json({ message: &#39;Invalid credentials&#39; }));&#xA;    })&#xA;  );&#xA;  &#xA;  render(&lt;LoginForm /&gt;);&#xA;  &#xA;  // کاربر ایمیل و پسورد را تایپ می‌کند&#xA;  await userEvent.type(screen.getByLabelText(/email/i), &#39;test@example.com&#39;);&#xA;  await userEvent.type(screen.getByLabelText(/password/i), &#39;wrongpass&#39;);&#xA;  &#xA;  // روی دکمه ورود کلیک می‌کند&#xA;  await userEvent.click(screen.getByRole(&#39;button&#39;, { name: /login/i }));&#xA;  &#xA;  // پیام خطا را می‌بیند&#xA;  expect(await screen.findByText(/invalid credentials/i)).toBeInTheDocument();&#xA;});نکته کلیدی اینجا این است که ما هیچ وقت کامپوننت را به صورت مجزا تست نمی‌کنیم. بلکه کل رفتار کاربر را شبیه‌سازی می‌کنیم. این تست‌ها کمی کندتر از تست واحد هستند اما بسیار ارزشمندترند چون اعتماد ما را به عملکرد صحیح برنامه بالا می‌برند.لایه سوم: تست سرتاسری (E2E) با Playwrightتست E2E مانند یک ربات است که مرورگر واقعی را باز می‌کند و دقیقا مثل یک کاربر واقعی با برنامه تعامل می‌کند. این تست‌ها در محیطی مشابه محیط تولید اجرا می‌شوند.با Playwright، من می‌توانم همزمان در کروم، فایرفاکس و وب‌کیت تست کنم. این تست‌ها گران‌ترین و کندترین تست‌ها هستند، بنابراین تعداد آنها را محدود نگه می‌دارم.معمولا سناریوهای اصلی کسب‌وکار را با E2E پوشش می‌دهم. مثلا:ثبت‌نام کاربر جدیدورود به سیستمخرید یک محصولخروج از سیستمjavascript// مثال از تست E2E با Playwright&#xA;test(&#39;user can complete purchase flow&#39;, async ({ page }) =&gt; {&#xA;  // باز کردن صفحه اصلی&#xA;  await page.goto(&#39;/&#39;);&#xA;  &#xA;  // انتخاب محصول&#xA;  await page.click(&#39;text=Product X&#39;);&#xA;  await page.click(&#39;button:has-text(&#34;Add to Cart&#34;)&#39;);&#xA;  &#xA;  // رفتن به سبد خرید&#xA;  await page.click(&#39;a:has-text(&#34;Cart&#34;)&#39;);&#xA;  await page.click(&#39;button:has-text(&#34;Checkout&#34;)&#39;);&#xA;  &#xA;  // پر کردن فرم&#xA;  await page.fill(&#39;input[name=&#34;email&#34;]&#39;, &#39;test@example.com&#39;);&#xA;  await page.fill(&#39;input[name=&#34;address&#34;]&#39;, &#39;123 Main St&#39;);&#xA;  await page.click(&#39;button:has-text(&#34;Confirm Purchase&#34;)&#39;);&#xA;  &#xA;  // تایید موفقیت&#xA;  await expect(page.locator(&#39;text=Order Confirmed&#39;)).toBeVisible();&#xA;});نسبت و تعادل بین این سه لایهمن به عنوان یک توسعه‌دهنده ارشد، از قانون هرم تست پیروی می‌کنم. نسبت تست‌ها به این شکل است:تست واحد: حدود 70 درصد تست‌هاتست یکپارچه‌سازی: حدود 20 درصد تست‌هاتست E2E: حدود 10 درصد تست‌هاچرا؟ چون تست‌های واحد سریع و ارزان هستند و هر تغییری را سریع نشان می‌دهند. تست‌های یکپارچه‌سازی ارزش بالایی دارند اما کندترند. و تست‌های E2E گران‌ترین هستند و فقط برای مسیرهای اصلی استفاده می‌شوند.نکات پیشرفته برای مصاحبهاگر می‌خواهید در مصاحبه بدرخشید، این نکات را به خاطر داشته باشید:۱. من همیشه تست‌ها را همزمان با توسعه می‌نویسم، نه بعد از آن. این کار باعث می‌شود کد من از ابتدا تست‌پذیر طراحی شود.۲. برای کامپوننت‌هایی که به Next.js متکی هستند (مثل useRouter یا usePathname)، من از Mock کردن استفاده می‌کنم. در Next.js 13 و بالاتر، از NextRouter مخصوص تست استفاده می‌کنم.۳. من هرگز از snapshot testing به عنوان تست اصلی استفاده نمی‌کنم. اسنپ‌شات‌ها فقط برای بررسی تغییرات ناخواسته در UI مفیدند، نه برای تایید منطق.۴. در پروژه‌های بزرگ، من تست‌ها را بر اساس پوشش کد (code coverage) ارزیابی نمی‌کنم. به جای آن، کیفیت سناریوهای تست شده را بررسی می‌کنم. ۱۰۰ درصد پوشش کد به معنای ۱۰۰ درصد کیفیت نیست.۵. من از فیکسچرها (fixtures) و فکتوری‌ها برای ساخت داده‌های تست استفاده می‌کنم تا تست‌ها خواناتر و قابل نگهداری‌تر باشند.یک سناریوی واقعی برای مصاحبهفرض کنید یک کامپوننت لیست کاربران داریم که داده‌ها را از API می‌گیرد و با Zustand مدیریت state می‌کند. من برای این کامپوننت، سه نوع تست می‌نویسم:۱. تست واحد برای خود تابع fetchUsers۲. تست یکپارچه‌سازی برای کامپوننت UserList که نشان می‌دهد در حالت loading، حالت error، و حالت موفقیت چه رفتاری دارد۳. تست E2E برای کل فرآیند لاگین و مشاهده لیست کاربران</description><category>مصطفی رستگار</category><author>مصطفی رستگار</author><pubDate>Sat, 15 Aug 2026 13:11:12 +0330</pubDate></item><item><title>04- Lighthouse و Web Vitals (اندازه‌گیری در Production)</title><link>https://virgool.ir/@m.rastegar991/04-lighthouse-و-web-vitals-اندازه-گیری-در-production-indrz7rbsfec</link><description>سناریو: مدیر محصول به شما می گوید &#34;کاربران می گویند سایت کند است، اما من که تست می کنم سریع است!&#34; شما چطور این مسئله را با استفاده از Lighthouse و Web Vitals در محیط Production ریشه یابی می کنید؟پاسخ جامع و روان برای مصاحبه:برای پاسخ به این سوال، باید قضیه را از دو زاویه متفاوت نگاه کنیم: یکی ابزارهای اندازه گیری در محیط آزمایشگاهی (Lab) و دیگری اندازه گیری در محیط واقعی (Field). تفاوت این دو، کلید تشخیص مشکل است.۱. تفاوت اساسی را توضیح می دهم (سناریو محور):اول از همه به مدیر محصول توضیح می دهم که تست من روی دستگاه خودم با اینترنت پرسرعت و پردازنده قوی، یک تست Lab محسوب می شود. اما کاربران من با گوشی های میان رده و شبکه اینترنت ضعیف در حال استفاده هستند. اینجاست که Web Vitals در محیط Production وارد بازی می شود.۲. ابزار دقیق (Instrumentation) در Production:به جای اینکه فقط یک بار سایت را با Lighthouse در مرورگر خودم تست کنم، باید یک سیستم پایش (Monitoring) راه بیندازم. کار من به عنوان مهندس ارشد این است که کدهای اندازه گیری را به صورت مستقیم در برنامه Next.js تزریق کنم.من از کتابخانه رسمی web-vitals استفاده می کنم.این کتابخانه را در فایل _app.tsx یا layout.tsx قرار می دهم تا تمامی مسیرها را پوشش دهد.سپس این داده ها را به یک سرویس تحلیلی مانند Google Analytics 4، New Relic یا DataDog می فرستم. هدف من این است که ببینم ۷۵ درصد کاربران من (Percentile 75) چه تجربه ای دارند، نه اینکه فقط یک کاربر خاص چه تجربه ای دارد.۳. سه متریک طلایی (Core Web Vitals) را رصد می کنم:من دقیقا به سه عدد کلیدی که گوگل تعیین کرده است نگاه می کنم و برای هر کدام یک سناریو تعریف می کنم:LCP (Largest Contentful Paint) یا بزرگترین محتوای قابل مشاهده: اگر این عدد بالای ۲.۵ ثانیه باشد، یعنی تصویر اصلی یا هدر سایت دیر بارگذاری می شود. در Next.js این مشکل را با استفاده از کامپوننت next/image با ویژگی priority برای مهم ترین تصاویر حل می کنم.FID (First Input Delay) یا تاخیر در اولین ورودی: این متریک نشان می دهد که وقتی کاربر برای اولین بار روی دکمه ای کلیک می کند، چقدر طول می کشد تا مرورگر پاسخ دهد. اگر این عدد بالا باشد، نشان می دهد که ترد اصلی (Main Thread) مرورگر قفل شده است. در Next.js این یعنی من باید کدهای سنگین را با استفاده از useEffect یا کتابخانه هایی مانند next/dynamic برای بارگذاری غیرهمزمان (Lazy Loading) به تعویق بیندازم تا پردازنده آزاد شود.CLS (Cumulative Layout Shift) یا جابه جایی ناگهانی اجزای صفحه: این بدترین تجربه کاربری است. یعنی کاربر در حال خواندن مطلب است که ناگهان یک بنر پایین می پرد و صفحه پایین می رود. من در Production این را رصد می کنم تا ببینم آیا ابعاد تصاویر و ویدیوها را به درستی مشخص کرده ام یا اینکه تبلیغات متفرقه باعث این لرزش می شوند.۴. از قدرت Next.js در Production استفاده می کنم:به عنوان کسی که با Next.js کار می کند، من فقط منتظر داده های کاربران نمی مانم. من از قابلیت های خود فریم ورک برای بهبود این اعداد استفاده می کنم:مقایسه استاتیک و داینامیک: اگر صفحه ای کاملا استاتیک است، حتما آن را ssg به صورت کامل در زمان Build تولید می کنم تا زمان LCP به حداقل برسد.استفاده از Incremental Static Regeneration (ISR): برای صفحاتی که داده هایشان هر چند دقیقه یک بار تغییر می کند، از ISR استفاده می کنم تا همیشه یک فایل HTML آماده برای اولین بازدید وجود داشته باشد و نیازی به اجرای کد سمت سرور نباشد.۵. جمع بندی نهایی برای مصاحبه:در نهایت به کارفرما یا تیم می گویم که Lighthouse (تست آزمایشگاهی) برای من یک چراغ راهنما است که به من می گوید &#34;ممکن است&#34; مشکل داشته باشم. اما Web Vitals در Production (با استفاده از داده های واقعی RUM - Real User Monitoring) به من می گوید که &#34;واقعا&#34; کدام کاربران و در کدام منطقه جغرافیایی دچار مشکل هستند. من با اتکا به این داده ها، تصمیم می گیرم که مثلا از CDN بهتری استفاده کنم، یا اسکریپت های تحلیلی شخص ثالث را به روش defer یا lazyOnload در Next.js بارگذاری کنم تا تداخلی با رندر اصلی سایت نداشته باشند.این روش نشان می دهد که من فقط یک کدنویس نیستم، بلکه به UX و عملکرد محصول در دنیای واقعی فکر می کنم.</description><category>مصطفی رستگار</category><author>مصطفی رستگار</author><pubDate>Tue, 11 Aug 2026 16:16:09 +0330</pubDate></item><item><title>03- Performance Metrics (LCP, FID, CLS, TTI, TBT)</title><link>https://virgool.ir/@m.rastegar991/03-performance-metrics-lcp-fid-cls-tti-tbt-fd09ajl0kntg</link><description>عملکرد وب و معیارهای کلیدی (Core Web Vitals)اجازه بدهید این مفاهیم را با یک تشبیه ساده شروع کنم. فرض کنید کاربر وارد یک رستوران (وبسایت) می‌شود:LCP (Largest Contentful Paint) - بزرگترین محتوای قابل مشاهدهاین معیار زمان بارگذاری بزرگترین المان قابل مشاهده در صفحه را اندازه می‌گیرد. مثل این می‌ماند که مشتری تا کی باید منتظر بماند تا غذای اصلی اش روی میز بیاید. در یک وبسایت، این المان معمولا تصویر اصلی (Hero Image) یا عنوان بزرگ است. استاندارد خوب یعنی زیر ۲.۵ ثانیه. اگر بالاتر باشد، کاربر حس می‌کند سایت کند است.FID (First Input Delay) - تاخیر اولین ورودیاین معیار فاصله زمانی بین اولین تعامل کاربر (مثلا کلیک روی دکمه) تا زمانی که مرورگر بتواند به آن واکنش نشان دهد را اندازه می‌گیرد. مثل این است که مشتری به پیشخدمت علامت می‌دهد اما پیشخدمت چند ثانیه طول می‌کشد تا متوجه شود و پاسخ دهد. استاندارد خوب زیر ۱۰۰ میلی ثانیه است. این معیار نشان می‌دهد که مرورگر چقدر مشغول پردازش کارهای سنگین است.CLS (Cumulative Layout Shift) - تغییرات ناگهانی چیدماناین معیار میزان جابجایی غیرمنتظره المان‌های صفحه را اندازه می‌گیرد. فرض کنید مشتری می‌خواهد دکمه سفارش را بزند اما ناگهان یک بنر تبلیغاتی پایین می‌آید و دکمه جابجا می‌شود و مشتری روی چیز دیگری کلیک می‌کند. استاندارد خوب زیر ۰.۱ است.TTI (Time to Interactive) - زمان تا تعامل‌پذیریاین معیار نشان می‌دهد صفحه از چه زمانی کاملا آماده تعامل با کاربر است. یعنی هم محتوا نشان داده شده و هم event listener ها متصل شده‌اند و کاربر می‌تواند با همه المان‌ها کار کند. استاندارد خوب زیر ۵ ثانیه است.TBT (Total Blocking Time) - زمان کل مسدودیتاین معیار مجموع زمانی است که صفحه به دلیل پردازش‌های سنگین جاوااسکریپت، قادر به پاسخ‌گویی به ورودی‌های کاربر نیست. هر بخش از پردازش که بیش از ۵۰ میلی ثانیه طول بکشد، به عنوان زمان مسدودیت محسوب می‌شود. استاندارد خوب زیر ۳۰۰ میلی ثانیه است.سناریوی مصاحبه: چطور این معیارها را در Next.js بهبود می‌دهیم؟فرض کنید مدیر محصول به شما می‌گوید که LCP سایت ما ۴ ثانیه است و CLS هم ۰.۲۵ است. راه‌حل شما چیست؟برای بهبود LCP:۱. از Image کامپوننت Next.js با priority استفاده کنید تا تصاویر اصلی زودتر بارگذاری شوند۲. از SSR یا ISR به جای CSR برای صفحات اصلی استفاده کنید۳. فایل‌های جاوااسکریپت را با dynamic import به بخش‌های کوچک تقسیم کنید۴. از CDN برای بارگذاری assets استفاده کنیدبرای بهبود FID و TBT:۱. کدهای سنگین را با استفاده از Web Workers به ترد جداگانه ببرید۲. از useDeferredValue و useTransition در React برای اولویت‌بندی رندرها استفاده کنید۳. اجرای کدهای غیرضروری را به بعد از بارگذاری صفحه موکول کنید (با useEffect و setTimeout)برای بهبود CLS:۱. برای همه تصاویر و ویدیوها، width و height مشخص کنید۲. از placeholder های skeleton استفاده کنید تا فضای المان‌ها قبل از بارگذاری مشخص باشد۳. از font-display: optional برای فونت‌ها استفاده کنید تا متن با فونت سیستمی نشان داده شود و بعدا جایگزین شودمثال عملی در Next.js:jsx// بهبود LCP با اولویت تصویر&#xA;import Image from &#39;next/image&#39;&#xA;&#xA;&lt;Image &#xA;  src=&#34;/hero.jpg&#34; &#xA;  priority // این باعث می‌شود تصویر زودتر بارگذاری شود&#xA;  width={1200} &#xA;  height={600}&#xA;  placeholder=&#34;blur&#34; // کاهش CLS&#xA;/&gt;&#xA;&#xA;// بهبود TTI با dynamic import&#xA;const HeavyComponent = dynamic(() =&gt; import(&#39;./HeavyComponent&#39;), {&#xA;  loading: () =&gt; &lt;Skeleton /&gt;,&#xA;  ssr: false // اگر نیازی به SSR نداشته باشد&#xA;})&#xA;&#xA;// بهبود FID با useTransition&#xA;const [isPending, startTransition] = useTransition()&#xA;const handleSearch = (value) =&gt; {&#xA;  startTransition(() =&gt; {&#xA;    setSearchResults(filterData(value)) // این عملیات سنگین را به اولویت پایین‌تر می‌دهد&#xA;  })&#xA;}نکته مهم این است که این معیارها به هم وابسته هستند. مثلا اگر TBT بالا باشد، FID هم بد می‌شود چون مرورگر مشغول است. یا اگر تصاویر با اندازه مشخص نباشند، CLS بالا می‌رود.در نهایت برای اندازه‌گیری این معیارها از ابزارهای زیر استفاده می‌کنیم:Lighthouse در DevToolsCore Web Vitals گزارش شده در Google Search Consoleکتابخانه web-vitals در Next.js که می‌تواند داده‌های واقعی کاربران را جمع‌آوری کند</description><category>مصطفی رستگار</category><author>مصطفی رستگار</author><pubDate>Tue, 11 Aug 2026 16:06:40 +0330</pubDate></item><item><title>02- Code Splitting استراتژیها (Route-based, Component-based)</title><link>https://virgool.ir/@m.rastegar991/02-code-splitting-استراتژیها-route-based-component-based-rbxyxamcu1li</link><description>پاسخ پیشنهادی (برای بیان در مصاحبه):۱. تعریف کلی (جلب توجه مصاحبهگر):«Code Splitting یعنی به جای اینکه کل برنامه را یک جا به کاربر بفرستیم، آن را به چند تکه کوچکتر تقسیم میکنیم و فقط همان چیزی را میفرستیم که کاربر در همان لحظه به آن نیاز دارد. این کار باعث میشود صفحه اول خیلی سریعتر بارگذاری شود.»۲. استراتژی اول: Route-based Splitting (مهمترین و رایجترین)«در این روش، براساس آدرسهای برنامه (صفحات) کد را تقسیم میکنیم. مثلاً وقتی کاربر وارد صفحه «داشبورد» میشود، کد مربوط به صفحه «تنظیمات» را اصلاً دریافت نمیکند. در Next.js این کار به صورت پیشفرض با استفاده از ساختار pages یا app انجام میشود. یعنی هر صفحه، یک chunk مجزا دارد.»مثال عملی در Next.js:javascript// در Next.js، خودِ سیستم این کار را میکند&#xA;// اما اگر بخواهیم دستی در React پیاده کنیم:&#xA;&#xA;const Dashboard = React.lazy(() =&gt; import(&#39;./pages/Dashboard&#39;));&#xA;const Settings = React.lazy(() =&gt; import(&#39;./pages/Settings&#39;));&#xA;&#xA;// حالا وقتی کاربر به /dashboard میرود، فقط Dashboard بارگذاری میشود۳. استراتژی دوم: Component-based Splitting (جزئی تر و هوشمندانه تر)«اینجا دیگر براساس صفحه نیست، بلکه براساس کامپوننت های سنگین یا کم استفاده تقسیم میکنیم. مثلاً یک مودال بزرگ، یک چارت سنگین یا یک ویرایشگر متن که شاید اصلاً همه کاربران از آن استفاده نکنند. این کامپوننت را فقط وقتی بارگذاری میکنیم که کاربر واقعاً به آن نیاز داشته باشد (مثلاً با کلیک دکمه).»مثال عملی:javascript// بارگذاری یک کامپوننت سنگین فقط هنگام کلیک&#xA;const HeavyChart = React.lazy(() =&gt; import(&#39;./HeavyChart&#39;));&#xA;&#xA;function Dashboard() {&#xA;  const [showChart, setShowChart] = useState(false);&#xA;&#xA;  return (&#xA;    &lt;div&gt;&#xA;      &lt;button ={() =&gt; setShowChart(true)}&gt;نمایش نمودار&lt;/button&gt;&#xA;      {showChart &amp;&amp; (&#xA;        &lt;Suspense fallback={&lt;div&gt;در حال بارگذاری...&lt;/div&gt;}&gt;&#xA;          &lt;HeavyChart /&gt;&#xA;        &lt;/Suspense&gt;&#xA;      )}&#xA;    &lt;/div&gt;&#xA;  );&#xA;}۴. تفاوت کلیدی این دو استراتژی (نکته طلایی برای تمایز):Route-basedComponent-basedدر سطح صفحه در سطح کامپوننت هنگام تغییر آدرس هنگام رویداد (مثل کلیک)بیشتر برای LCP مفید است بیشتر برای تعاملات ثانویه مفید است در Next.js پیشفرض است باید با dynamic import پیاده سازی کنیم۵. نکته پیشرفته تر (برای نشان دادن دانش عمیق):«در Next.js میتوانیم با استفاده از next/dynamic کنترل بیشتری داشته باشیم. مثلاً میتوانیم مشخص کنیم که یک کامپوننت فقط در سمت کلاینت بارگذاری شود (برای کتابخانههایی که با SSR مشکل دارند) یا اینکه در حین بارگذاری، یک حالت Loading نمایش دهیم.»javascriptimport dynamic from &#39;next/dynamic&#39;;&#xA;&#xA;const DynamicChart = dynamic(&#xA;  () =&gt; import(&#39;../components/HeavyChart&#39;),&#xA;  { &#xA;    ssr: false, // فقط کلاینت&#xA;    loading: () =&gt; &lt;p&gt;در حال بارگذاری نمودار...&lt;/p&gt;&#xA;  }&#xA;);۶. جمع بندی نهایی (با نگاه مهندسی):«انتخاب استراتژی به این بستگی دارد که بزرگترین مشکل عملکردی ما چیست. اگر صفحه اول کند است → Route-based. اگر تعاملات داخلی کند است → Component-based. در پروژه های بزرگ معمولاً از ترکیب هر دو استفاده میکنیم. همچنین با ابزارهایی مثل @next/bundle-analyzer مدام سایز باندل را چک میکنیم تا مطمئن شویم که تقسیمبندی درست کار کرده است.»نکات اضافه برای مصاحبه (اگر سوال عمقی پرسیدند):ابزارهای مکمل: Webpack (با SplitChunksPlugin)، @next/bundle-analyzerچالشها: مدیریت حالت Loading با Suspense، جلوگیری از Flicker (پرش صفحه)معیار موفقیت: کاهش First Load JS در گزارش Lighthouse</description><category>مصطفی رستگار</category><author>مصطفی رستگار</author><pubDate>Tue, 11 Aug 2026 15:21:16 +0330</pubDate></item><item><title>01- State Management Strategy (کدام ابزار برای چه سناریویی؟)</title><link>https://virgool.ir/@m.rastegar991/01-state-management-strategy-کدام-ابزار-برای-چه-سناریویی-zzr0hpr4bnej</link><description> State Management Strategy (کدام ابزار برای چه سناریویی؟)اینجا یک پاسخ کاملاً روان، ساختاریافته و قابل‌ارائه در مصاحبه فنی برای سطح Senior آماده شده است. این پاسخ را طوری طراحی کرده‌ام که هم تسلط فنی شما را نشان دهد، هم تجربه‌ی عملی و هم قدرت تصمیم‌گیری معماری شما.پاسخ پیشنهادی برای مصاحبه (سناریو محور):«برای انتخاب ابزار مدیریت state، من هیچ‌وقت یک گزینه را به‌عنوان «برنده‌ی مطلق» در نظر نمی‌گیرم. استراتژی من بر اساس سه محور تصمیم‌گیری است: Scope (حوزه‌ی استفاده)، Frequency (تناوب تغییرات) و Complexity (پیچیدگی روابط داده‌ها).بیایید سناریوها را از ساده به پیچیده مرور کنم:۱. State محلی (Local State) – نیازی به ابزار نیستاگر data فقط در یک کامپوننت یا فرزندان مستقیم آن استفاده می‌شود (مثل وضعیت باز/بستن مودال یا مقدار یک فرم ساده)، من از useState یا useReducer خود React استفاده می‌کنم.نکته‌ی Senior: حتی برای فرم‌های پیچیده‌تر، اگر نیاز به اعتبارسنجی همزمان داشته باشم، از useReducer به‌همراه useContext محدود به همان زیردرخت استفاده می‌کنم تا از رندر مجدد کل اپلیکیشن جلوگیری کنم.۲. State اشتراکی بین چند کامپوننت (Shared / Global State) – دو ابزار اصلیاینجا بسته به سناریو، بین Redux Toolkit و Zustand یکی را انتخاب می‌کنم:اگر اپلیکیشن بزرگ است، تیم بزرگ است، و نیاز به رویدادنگاری (Logging)، زمان‌سفر (Time-travel) و میان‌افزار (Middleware) برای درخواست‌های غیرهمزمان داریم → Redux Toolkit را انتخاب می‌کنم.مثال: پنل مدیریتی با ده‌ها صفحه، تعداد زیادی API موازی، و نیاز به دیباگ دقیق اتفاقاتی که در طول زمان رخ داده.دلیل Senior: RTK به‌خاطر ساختار اجباری خود، باعث یکنواختی کد در تیم‌های بزرگ می‌شود و createAsyncThunk به‌خوبی وضعیت‌های loading/error را مدیریت می‌کند.اگر اپلیکیشن کوچک‌تر یا متوسط است، تیم چابک دارد، و بیشتر به سادگی و کاهش Boilerplate نیاز داریم → Zustand را ترجیح می‌دهم.مثال: یک داشبورد تحلیلی یا فروشگاه اینترنتی با تعداد محدودی اسلایس.دلیل Senior: Zustand نیازی به Provider ندارد، مستقیماً استفاده می‌شود، و به‌خاطر استفاده از subscribe، می‌توانم به‌راحتی از رندر مجدد اضافی جلوگیری کنم.۳. State سمت سرور (Server State) – کاملاً جداستاینجا کاملاً از React Query (یا TanStack Query) استفاده می‌کنم، نه Redux.دلیلش این است که: کش کردن، بازپرس‌گیری خودکار (Re-fetch)، مدیریت وضعیت‌های Stale-While-Revalidate، و همگام‌سازی با سرور، وظایفی هستند که Redux برایشان طراحی نشده و باعث می‌شود کدمان سرشار از Boilerplate و باگ شود.سناریوی عملی:اگر کاربری یک لیست را مشاهده می‌کند و سپس به صفحه‌ی جزئیات می‌رود، React Query به‌طور پیش‌فرض داده‌ها را کش می‌کند و در بازگشت، بدون درخواست مجدد، نمایش می‌دهد. این کار با Redux نیاز به نوشتن منطق کش و زمان‌انقضا دارد که اصلاً به‌صرفه نیست.۴. State مبتنی بر URL (Route State) – متعلق به Next.js استدر Next.js، من از URL Parameters، Search Params و Server Components به‌عنوان منبع حقیقت (Single Source of Truth) استفاده می‌کنم.مثال: فیلترهای جستجو یا شماره‌ی صفحه‌بندی را در useSearchParams ذخیره می‌کنم، چون کاربر می‌تواند لینک را به اشتراک بگذارد و صفحه با همان حالت بارگذاری شود.نکته‌ی مهم Senior: هرگز این داده‌ها را در Redux کپی نمی‌کنم تا از عدم هماهنگی (Out of Sync) جلوگیری کنم.۵. استراتژی ترکیبی (فراتر از ابزار) – اصل تفکیک وظایفدر پروژه‌های بزرگ، من از معماری لایه‌ای استفاده می‌کنم:لایه‌ی سرور: داده‌ها از طریق Server Components در Next.js و React Query (در سمت کلاینت) تأمین می‌شوند.لایه‌ی کلاینت جهانی: Zustand یا Redux فقط برای State های UI (مثل تم، وضعیت سایدبار، یا انتخاب‌های موقت کاربر) استفاده می‌شود.لایه‌ی فرم: از React Hook Form به‌همرافـت Zustand برای مدیریت State موقت فرم قبل از ارسال استفاده می‌کنم.جمع‌بندی نهایی برای مصاحبه‌گر:«به‌عنوان یک Senior، من مخالف استفاده از یک ابزار برای همه‌چیز هستم. استراتژی من این است:Server State → React QueryClient Global State → Zustand (برای سرعت توسعه) یا Redux Toolkit (برای مقیاس‌پذیری و دیباگ)URL State → Next.js RouterLocal State → React Hooks خالصمهم‌تر از انتخاب ابزار، تعیین مرزهای هر لایه و جلوگیری از نشت داده‌ها (Data Leakage) بین این لایه‌هاست. این کار باعث می‌شود تست‌پذیری افزایش یابد، پرفورمنس بهینه شود و تیم بتواند به‌صورت موازی روی بخش‌های مختلف کار کند.»</description><category>مصطفی رستگار</category><author>مصطفی رستگار</author><pubDate>Tue, 11 Aug 2026 12:59:12 +0330</pubDate></item><item><title>28- Cache Invalidation استراتژی‌ها (بر اساس tags و paths)</title><link>https://virgool.ir/@m.rastegar991/28-cache-invalidation-استراتژی-ها-بر-اساس-tags-و-paths-omkr4fzobfpr</link><description>برای مصاحبه فنی، بهترین روش این است که اول مسئله را توضیح دهی، بعد راه‌حل را، و در آخر مثال عملی بزنی. در ادامه یک متن آماده و روان برای بیان شفاهی آماده کرده‌ام:متن آماده برای مصاحبه (بیان شفاهی)«در Next.js، ما برای افزایش سرعت، داده‌ها را کش (Cache) می‌کنیم. اما مشکل اینجاست که وقتی داده‌ها در بک‌اند تغییر می‌کنند، کش ما قدیمی می‌شود. به این مشکل Stale Data می‌گوییم. برای حل این مشکل، از استراتژی‌های Cache Invalidation استفاده می‌کنیم که یعنی «چه زمانی کش را باطل و داده‌های تازه را جایگزین کنیم».»«در Next.js دو رویکرد اصلی داریم: بر اساس مسیر (Path-based) و بر اساس تگ (Tag-based).»۱. استراتژی بر اساس مسیر (Path-based Revalidation)«در این روش، به Next.js می‌گوییم که هر چند ثانیه یک‌بار، یک صفحه‌ی خاص را دوباره بساز. این کار با دو مقدار انجام می‌شود:»time-based در fetch یا revalidate در صفحه:«مثلاً اگر بنویسم revalidate: 60، یعنی هر ۶۰ ثانیه یک‌بار، این صفحه در پس‌زمینه ری‌جنریت می‌شود و دفعه‌ی بعد که کاربر بیاید، نسخه‌ی تازه را می‌بیند.»revalidatePath در سرور اکشن‌ها:«وقتی کاربر کاری انجام می‌دهد (مثل ثبت نظر)، من با revalidatePath(&#39;/posts&#39;) به Next.js می‌گویم که مسیر مربوطه را پاک کن و در دفعه‌ی بعد که درخواست شد، از نو بساز.»«مزیتش سادگی است، اما عیبش این است که اگر مسیرهای زیادی داشته باشیم، باید تک‌تک آن‌ها را مدیریت کنیم.»۲. استراتژی بر اساس تگ (Tag-based Revalidation)«روش دقیق‌تر، استفاده از تگ است. من هنگام دریافت داده، یک برچسب (تگ) به آن attach می‌کنم. مثلاً:»javascriptfetch(&#39;https://api.example.com/posts&#39;, {&#xA;  next: { tags: [&#39;posts&#39;] }&#xA;})«حالا هر جا که داده‌ی مربوط به پست‌ها تغییر کند، مثلاً یک پست جدید اضافه شود، من با دستور revalidateTag(&#39;posts&#39;) به Next.js می‌گویم: هر درخواستی که این تگ را دارد، کشش را باطل کن.»«مزیت بزرگ این روش این است که من نیازی نمی‌دانم داده در کدام مسیر استفاده شده است. Next.js خودش تمام مسیرهایی که از این تگ استفاده کرده‌اند را پیدا کرده و آن‌ها را ری‌جنریت می‌کند.»«این روش برای اپلیکیشن‌های بزرگ خیلی مناسب‌تر است، چون کش را به‌صورت دانه‌دانه (Granular) و بر اساس موضوع داده مدیریت می‌کنیم، نه بر اساس صفحه.»نکته‌ی کلیدی برای مصاحبه (جمع‌بندی)«نکته‌ی مهم این است که در Next.js 14 به‌بعد، هر دو روش در سرور کامپوننت‌ها و سرور اکشن‌ها قابل استفاده هستند. انتخاب بین این دو به این بستگی دارد که: اگر داده‌ی ما در چندین صفحه پراکنده است، Tag-based بهتر است و اگر فقط یک صفحه‌ی خاص را داریم، Path-based کافی است. همچنین می‌توانیم هر دو را با هم ترکیب کنیم.»اگر مصاحبه‌کننده سوال عمیق‌تر پرسید:تفاوت revalidatePath و revalidateTag چیست؟revalidatePath یک مسیر خاص را پاک می‌کند، ولی revalidateTag تمام مسیرهایی که از آن تگ استفاده کرده‌اند را پیدا کرده و پاک می‌کند.آیا می‌توانیم درخواست‌های client-side را هم مدیریت کنیم؟در سمت کلاینت، از کتابخانه‌هایی مثل React Query یا SWR استفاده می‌کنیم که با useQuery و invalidateQueries کار مشابهی را انجام می‌دهند، اما در Next.js بحث ما روی کش سرور است.</description><category>مصطفی رستگار</category><author>مصطفی رستگار</author><pubDate>Tue, 11 Aug 2026 12:48:32 +0330</pubDate></item><item><title>26- Streaming SSR و Suspense در Next.js (رندر تدریجی HTML و TTFB)</title><link>https://virgool.ir/@m.rastegar991/26-streaming-ssr-و-suspense-در-nextjs-رندر-تدریجی-html-و-ttfb-xqsj5eldpgui</link><description>قسمت Streaming SSR و Suspense۱. شروع مکالمه (بیان مشکل قدیمی):&#34;اگر بخواهم خیلی ساده توضیح بدهم، قبلاً در خیلی از فریمورک‌ها، وقتی کاربر درخواست یک صفحه را می‌داد، سرور باید تمام اطلاعات آن صفحه (مثل دیتای API) را از بیرون می‌گرفت، صبر می‌کرد تا همه چیز آماده شود، بعد کل HTML را یکجا تولید می‌کرد و برای کاربر می‌فرستاد. به این حالت SSR کلاسیک می‌گوییم.&#34;۲. مشکل این روش (مقدمه‌ای برای Streaming):&#34;مشکل اینجا بود که اگر یکی از اطلاعات سنگین یا کند بود، کل صفحه متوقف می‌شد و کاربر تا رسیدن کامل HTML، یک صفحه سفید می‌دید. این موضوع باعث می‌شد زمان اولین بایت (TTFB) خیلی بالا برود، چون مرورگر باید صبر می‌کرد تا کل کار سرور تمام شود.&#34;۳. راه‌حل Next.js (معرفی Streaming SSR):&#34;اینجاست که Next.js با قابلیت Streaming SSR وارد می‌شود. یعنی سرور دیگر منتظر نمی‌ماند تا همه چیز آماده شود. به محض اینکه بخش‌های سریع صفحه (مثل هدر و فوتر) آماده شدند، آنها را به صورت پاره‌پاره (Stream) و همان لحظه برای مرورگر می‌فرستد.&#34;۴. نقش Suspense (چگونه این کار ممکن می‌شود؟):&#34;این کار با کمک کامپوننت &lt;Suspense&gt; در React انجام می‌شود. ما بخش‌های کند صفحه را درون یک Suspense می‌پیچیم و یک fallback (مثل یک اسکلت‌لودینگ) برای آن تعریف می‌کنیم.&#34;۵. سناریوی اجرا (بیان روند گام‌به‌گام):&#34;وقتی کاربر صفحه را باز می‌کند:۱. سرور فوراً بخش‌های سریع را می‌فرستد و کاربر بلافاصله اسکلت اصلی صفحه را می‌بیند (TTFB خیلی پایین می‌آید).۲. برای بخشی که درون Suspense است، سرور یک placeholder (همان fallback) را در همان استریم اولیه می‌فرستد.۳. بعد از اینکه دیتای آن بخش خاص روی سرور آماده شد، Next.js قطعه HTML مربوط به همان بخش را به صورت جداگانه در همان استریم باز به مرورگر می‌فرستد.۴. React در مرورگر این قطعه جدید را تشخیص می‌دهد و جایگزین fallback می‌کند.&#34;۶. مزیت نهایی (جمع‌بندی برای مصاحبه‌کننده):&#34;نتیجه این کار دو مزیت بزرگ دارد:اولاً TTFB به شدت کاهش پیدا می‌کند، چون کاربر خیلی زودتر از قبل محتوای اولیه را می‌بیند.ثانیاً ما یک رندر تدریجی (Incremental Rendering) داریم. یعنی صفحه به مرور و قطعه‌قطعه کامل می‌شود، بدون اینکه کاربر احساس کند صفحه قفل شده است. این یعنی تجربه کاربری خیلی روان‌تر و شاخص‌های Core Web Vitals بهتری داریم.&#34;نکته طلایی برای مصاحبه‌کننده (اگر خیلی حرفه‌ای باشد):اگر مصاحبه‌کننده بپرسد: &#34;مگه فرقش با CSR چیه؟&#34; می‌توانید این را اضافه کنید:&#34;در CSR، همه چیز در مرورگر اتفاق می‌افتد و یک صفحه خالی می‌بینیم تا همه جاوااسکریپت لود شود. اما در Streaming SSR، محتوای HTML واقعی را از همان ابتدا به صورت جزیی دریافت می‌کنیم، که برای SEO و سرعت درک محتوا توسط کاربر خیلی حیاتی است.&#34;</description><category>مصطفی رستگار</category><author>مصطفی رستگار</author><pubDate>Tue, 11 Aug 2026 10:53:06 +0330</pubDate></item><item><title>25- Turbopack vs Webpack (تفاوت معماری و زمان بیلد)</title><link>https://virgool.ir/@m.rastegar991/25-turbopack-vs-webpack-تفاوت-معماری-و-زمان-بیلد-ngbfajdtlnta</link><description>مقدمه:&#34;هر دو Webpack و Turbopack، ابزارهای bundler (بسته‌ساز) هستند؛ یعنی کدهای ما (ماژول‌ها، کامپوننت‌ها، فایل‌های CSS و تصاویر) را گرفته و آنها را به فایل‌های بهینه‌شده برای مرورگر تبدیل می‌کنند. اما تفاوت اصلی در این است که Turbopark توسط تیم Vercel و مخصوص اکوسیستم Next.js ساخته شده تا جایگزین سریع‌تری برای Webpack باشد، مخصوصاً در محیط توسعه (Development).&#34;۱. تفاوت در معماری (چطور کار می‌کنند):Webpack (معماری قدیمی و مبتنی بر Node.js):Webpack در زمان اجرا، تمام فایل‌های پروژه را اسکن می‌کند، یک گراف وابستگی (Dependency Graph) می‌سازد و سپس هر فایل را یکی‌یکی با استفاده از لودرهای مختلف پردازش (ترنسفورم) می‌کند. این پردازش به صورت همزمان (synchronous) و با استفاده از تابع‌های JavaScript در محیط Node.js انجام می‌شود. به همین دلیل با افزایش تعداد فایل‌ها، زمان بیلد به شدت افزایش می‌یابد.Turbopack (معماری مدرن و مبتنی بر Rust):Turbopack با زبان Rust نوشته شده که بسیار سریع‌تر از JavaScript است. اما مهم‌تر از آن، از یک معماری Lazy Bundling یا بسته‌بندی تنبل استفاده می‌کند. یعنی فقط فایل‌هایی که در مرورگر درخواست می‌شوند یا تغییر کرده‌اند را پردازش می‌کند، نه کل پروژه را. همچنین از تکنیک‌های موازی‌سازی (Parallel Processing) و کش‌کردن بسیار هوشمندانه بهره می‌برد.۲. تفاوت در زمان بیلد (Build Time):Webpack:زمان بیلد در Webpack معمولاً با رشد پروژه، خطی (Linear) یا حتی نمایی افزایش می‌یابد. یعنی اگر تعداد فایل‌های شما دو برابر شود، زمان بیلد تقریباً دو برابر یا بیشتر می‌شود. در پروژه‌های بزرگ، زمان شروع سرور توسعه (Cold Start) ممکن است چندین دقیقه طول بکشد که برای تیم‌های بزرگ خسته‌کننده است.Turbopack:Turbopack زمان بیلد را به شدت کاهش می‌دهد. طبق تست‌های رسمی، در پروژه‌های بزرگ، Turbopack می‌تواند تا ۱۰ برابر سریع‌تر از Webpack باشد. دلیلش این است که فقط فایل‌های تغییر داده‌شده را بازسازی (Rebuild) می‌کند و از کش سخت‌افزاری سطح پایین (OS-level caching) استفاده می‌کند. نتیجه این می‌شود که سرور توسعه در کسری از ثانیه راه‌اندازی می‌شود و تجربه توسعه (Developer Experience) بسیار لذت‌بخش‌تری داریم.۳. جمع‌بندی نهایی برای مصاحبه:&#34;اگر بخواهم خیلی ساده بگویم:Webpack مثل یک کارگر خسته‌ای است که هر بار کل انبار را برای پیدا کردن یک جعبه می‌گردد، ولی Turbopack مثل یک ربات هوشمند است که فقط جعبه‌ای که نیاز داریم را از قبل آماده کرده و بقیه را تا وقتی نیاز نشوند، دست نمی‌زند. به همین دلیل Turbopack برای پروژه‌های جدید Next.js توصیه می‌شود، اما باید توجه داشت که در حال حاضر (نسخه ۱۳ به بالا) بیشتر برای محیط توسعه (Development) بهینه شده است و برای بیلد نهایی (Production) همچنان از Webpack استفاده می‌شود، هرچند که در آینده این تغییر خواهد کرد.&#34;نکات کلیدی که حتماً در مصاحبه بگویید:زبان Rust در مقابل JavaScript.معماری Lazy bundling در Turbopack.کاهش زمان Cold Start و Hot Reload.مناسب برای محیط توسعه، نه الزاماً تولید نهایی (تا الان).</description><category>مصطفی رستگار</category><author>مصطفی رستگار</author><pubDate>Mon, 10 Aug 2026 16:42:55 +0330</pubDate></item><item><title>24- Module Federation در Next.js (Micro-frontend)</title><link>https://virgool.ir/@m.rastegar991/24-module-federation-در-nextjs-micro-frontend-yqvvgowd1wgf</link><description>«ماژول فدریشن یک تکنیک در دنیای جاوااسکریپت است که به ما اجازه می‌دهد چند اپلیکیشن مجزا را در حین اجرا (Runtime) به هم متصل کنیم. یعنی بدون اینکه نیاز به دیپلوی همزمان داشته باشیم، یک اپلیکیشن می‌تواند کامپوننت‌ها یا صفحات را از اپلیکیشن دیگر بخواند و استفاده کند.در Next.js، این قابلیت به کمک پلاگین‌هایی مثل @module-federation/nextjs-mf پیاده‌سازی می‌شود. این یعنی تیم‌های مختلف می‌توانند هرکدام روی یک بخش از سایت کار کنند (مثل صفحه محصول، سبد خرید یا پنل کاربری) و این بخش‌ها را به صورت مستقل توسعه و دیپلوی کنند.در نهایت، یک اپلیکیشن هاست (Host) این بخش‌ها را کنار هم قرار می‌دهد و به کاربر نشان می‌دهد. مزیت اصلی این کار این است که اگر تیم سبد خرید، تغییر جدیدی دهد، فقط همان بخش آپدیت می‌شود و نیازی به دیپلوی کل پروژه نیست. به این معماری، میکرو-فرانت‌اند می‌گوییم.»نکات کلیدی که در ادامه می‌توانید اضافه کنیداگر مصاحبه‌کننده سوالات بیشتری پرسید، این نکات را هم مطرح کنید:یک مثال عینی برای توضیح بهتر«فرض کنید یک فروشگاه اینترنتی داریم. تیم A روی صفحه محصول کار می‌کند، تیم B روی سبد خرید و تیم C روی بخش نظرات. هر تیم به صورت جداگانه کد می‌زند و دیپلوی می‌کند. اما وقتی کاربر وارد سایت می‌شود، اپلیکیشن اصلی (Host) این سه بخش را از سرورهای مختلف دریافت کرده و در کنار هم نمایش می‌دهد. این کار باعث می‌شود سرعت توسعه بالا برود و ریسک دیپلوی کاهش پیدا کند.»</description><category>مصطفی رستگار</category><author>مصطفی رستگار</author><pubDate>Mon, 10 Aug 2026 16:35:09 +0330</pubDate></item><item><title>23- React Server Components Payload (انتقال داده سرور به کلاینت)</title><link>https://virgool.ir/@m.rastegar991/23-react-server-components-payload-انتقال-داده-سرور-به-کلاینت-rpah9aushey4</link><description>سناریو: نمایش لیست محصولات با قیمتگذاری لحظهایفرض کنید اپلیکیشن فروشگاهی داریم که قیمت محصولات باید از دیتابیس خوانده شود و با تخفیفهای سروری محاسبه گردد.1. کامپوننت اصلی (صفحه) - (Server Component)tsx// app/products/page.tsx (این یک Server Component است)&#xA;import { getProducts } from &#39;@/lib/database&#39;;&#xA;import { ProductListClient } from &#39;./ProductListClient&#39;;&#xA;&#xA;// این تابع فقط در سرور اجرا میشود و هرگز به مرورگر نمیرود&#xA;async function calculateDiscountedPrice(basePrice: number) {&#xA;  &#39;use server&#39;;&#xA;  // محاسبات سنگین روی سرور (مثلاً اعمال تخفیف ویژه)&#xA;  return basePrice * 0.8; &#xA;}&#xA;&#xA;export default async function ProductsPage() {&#xA;  // 1. گرفتن داده از دیتابیس در سمت سرور&#xA;  const products = await getProducts();&#xA;  &#xA;  // 2. پردازش سنگین روی سرور (محاسبه قیمت نهایی)&#xA;  const processedProducts = await Promise.all(&#xA;    products.map(async (p) =&gt; ({&#xA;      ...p,&#xA;      finalPrice: await calculateDiscountedPrice(p.price)&#xA;    }))&#xA;  );&#xA;&#xA;  // 3. ارسال دادههای پردازششده به کامپوننت کلاینت&#xA;  return &lt;ProductListClient initialProducts={processedProducts} /&gt;;&#xA;}2. کامپوننت سمت کلاینت (برای تعامل با کاربر)tsx// app/products/ProductListClient.tsx (این یک Client Component است)&#xA;&#39;use client&#39;;&#xA;&#xA;import { useState } from &#39;react&#39;;&#xA;&#xA;// این کامپوننت در مرورگر اجرا میشود&#xA;export function ProductListClient({ initialProducts }: { initialProducts: any[] }) {&#xA;  const [sortOrder, setSortOrder] = useState(&#39;asc&#39;);&#xA;  &#xA;  // مرتبسازی فقط در سمت کلاینت انجام میشود (بدون رفتن به سرور)&#xA;  const sortedProducts = [...initialProducts].sort((a, b) =&gt; {&#xA;    return sortOrder === &#39;asc&#39; &#xA;      ? a.finalPrice - b.finalPrice &#xA;      : b.finalPrice - a.finalPrice;&#xA;  });&#xA;&#xA;  return (&#xA;    &lt;div&gt;&#xA;      &lt;button ={() =&gt; setSortOrder(prev =&gt; prev === &#39;asc&#39; ? &#39;desc&#39; : &#39;asc&#39;)}&gt;&#xA;        تغییر ترتیب قیمت&#xA;      &lt;/button&gt;&#xA;      &#xA;      &lt;ul&gt;&#xA;        {sortedProducts.map(product =&gt; (&#xA;          &lt;li key={product.id}&gt;&#xA;            {product.name} - قیمت: {product.finalPrice} تومان&#xA;          &lt;/li&gt;&#xA;        ))}&#xA;      &lt;/ul&gt;&#xA;    &lt;/div&gt;&#xA;  );&#xA;}حالا بریم سراغ توضیح Payload در این مثال:در مصاحبه، دقیقاً به این کدها اشاره کنید و بگویید:&#34;وقتی کاربر درخواست میدهد، سرور تابع getProducts و calculateDiscountedPrice را اجرا میکند. اما سوال اینجاست که چه چیزی برای مرورگر فرستاده میشود؟&#34;سپس این تصویر را ترسیم کنید:سرور یک Payload میسازد که شامل این موارد است:HTML اولیه: ساختار &lt;div&gt; و &lt;ul&gt; که بلافاصله نمایش داده میشود.دادههای JSON فشرده: یعنی همان initialProducts که شامل id، name و finalPrice است. (دقت کنید که finalPrice قبلاً در سرور محاسبه شده، پس نیازی به ارسال basePrice یا منطق تخفیف به مرورگر نیست).این Payload به مرورگر میرود و مرورگر:اول HTML را نمایش میدهد (کاربر سریعاً صفحه را میبیند).بعد با استفاده از دادههای داخل Payload، کامپوننت ProductListClient را فعال میکند (Hydration).نکته مهم در این مثال: تابع calculateDiscountedPrice هرگز به مرورگر فرستاده نمیشود. فقط نتیجهی آن (یعنی finalPrice) داخل Payload قرار میگیرد. این یعنی حجم Payload بسیار کم است و کدهای سنگین سرور، محرمانه و امن میمانند.یک نکته عالی برای مصاحبه (Streaming Payload):اگر میخواهید خیلی حرفهای شوید، این را هم اضافه کنید:&#34;در Next.js، این Payload حتی به صورت Streaming هم قابل ارسال است. مثلاً اگر ۱۰۰۰ محصول داشته باشیم، نیازی نیست منتظر بمانیم تا سرور همه را پردازش کند. به محض اینکه ۱۰ محصول اول آماده شد، Payload مربوط به آنها به مرورگر میرود و کاربر میتواند آنها را ببیند، در حالی که بقیه محصولات هنوز در حال پردازش روی سرور هستند.&#34;برای نشان دادن این قابلیت، کد را اینطور تغییر دهید:tsx// app/products/page.tsx&#xA;import { Suspense } from &#39;react&#39;;&#xA;import { ProductsList } from &#39;./ProductsList&#39;;&#xA;import { ProductsSkeleton } from &#39;./ProductsSkeleton&#39;;&#xA;&#xA;export default function ProductsPage() {&#xA;  return (&#xA;    &lt;Suspense fallback={&lt;ProductsSkeleton /&gt;}&gt;&#xA;      {/* این کامپوننت به صورت غیرهمزمان (Stream) بارگذاری میشود */}&#xA;      &lt;ProductsList /&gt; &#xA;    &lt;/Suspense&gt;&#xA;  );&#xA;}خلاصهای که در مصاحبه میگویید:&#34;در این مثال، Payload چیزی نیست جز initialProducts که به همراه HTML به کلاینت فرستاده میشود. سرور تمام منطق سنگین را انجام داده و فقط خروجی نهایی را میفرستد. این یعنی کاربر سریعتر صفحه را میبیند، کد کمتری به مرورگر میرود و ما امنیت بیشتری داریم، چون فرمول تخفیف ما در سرور پنهان میماند.&#34;</description><category>مصطفی رستگار</category><author>مصطفی رستگار</author><pubDate>Sun, 09 Aug 2026 15:12:50 +0330</pubDate></item><item><title>22- Concurrent Rendering و Transitions useTransition, useDeferredValue)</title><link>https://virgool.ir/@m.rastegar991/22-concurrent-rendering-و-transitions-usetransition-usedeferredvalue-saodbwu05kzf</link><description>متن پیشنهادی برای مصاحبه«برای شروع بهتر است بگویم که هدف اصلی قابلیتهای جدید React، یعنی Concurrent Rendering و Transitions، بهبود تجربه کاربری (UX) و حفظ واکنشگرایی (Responsiveness) برنامه است، مخصوصاً وقتی با دادههای سنگین یا پردازشهای زمانبر مواجه میشویم.»۱. مشکل قدیمی (همزمانی blocking)«در نسخههای قدیمی React، رندرینگ به صورت همزمان (Synchronous) و غیرقابل وقفه بود. یعنی اگر کاربر دکمهای را میزد که باعث فیلتر شدن یک لیست ۱۰۰۰ تایی میشد، صفحه تا وقتی که این پردازش و رندرینگ تمام نشود، قفل (Block) میشد. در این حالت، کاربر نمیتوانست تایپ کند، اسکرول کند یا دکمه دیگری بزند. این یعنی تجربه کاربری ضعیف.»۲. راهحل: رندرینگ همزمان (Concurrent Rendering)«React 18 با معرفی Concurrent Rendering این مشکل را حل کرد. یعنی موتور رندرینگ React دیگر یک پروسه یکباره نیست؛ بلکه میتواند کار را متوقف کند، به عقب برگردد یا اولویتها را جابهجا کند. در واقع React به ما این امکان را میدهد که بهش بگوییم: &#34;این آپدیت جدید خیلی فوری نیست، اگر کار فوریتری پیش آمد، اول آن را انجام بده.&#34;»۳. ابزار اصلی: useTransition«برای پیادهسازی این قابلیت، از هوک useTransition استفاده میکنیم. این هوک دو چیز به ما میدهد: یک isPending (برای نمایش اسپینر یا حالت بارگذاری) و یک تابع startTransition.»نکته کلیدی برای گفتن: «همه آپدیتهایی که داخل startTransition قرار میدهیم، به عنوان آپدیت غیرفوری (Low-priority) در نظر گرفته میشوند.»مثال عملی:«فرض کنید یک اپلیکیشن جستجوی محصولات داریم. وقتی کاربر در اینپوت تایپ میکند، ما تابع setKeyword را داخل startTransition میگذاریم. با این کار، وقتی کاربر تایپ میکند، واکنش صفحه (تغییرات اینپوت) سریع انجام میشود، ولی لیست محصولات با کمی تأخیر و وقتی CPU آزاد شد، آپدیت میشود. در همین حین، با استفاده از isPending به کاربر یک لودینگ ساده نشان میدهیم تا متوجه شود که برنامه بیکار نشده است.»۴. ابزار دوم: useDeferredValue«ابزار دیگر، هوک useDeferredValue است. این هوک شبیه useTransition عمل میکند، با این تفاوت که به جای اینکه ما تصمیم بگیریم کدام عملیات غیرفوری باشد، یک مقدار (State) را به آن میدهیم و از React میخواهیم که یک کپی از آن مقدار را با اولویت پایینتر برای ما نگه دارد.»مثال تفاوت:useTransition: برای زمانی که کنترل اجرا را در دست دارید (مثل یک Event Handler).useDeferredValue: برای زمانی که مقدار از بیرون (مثل Props) به شما میرسد و شما میخواهید رندر آن را به تأخیر بیندازید، بدون اینکه خودتان تابع آپدیت را صدا بزنید.جمعبندی نهایی برای مصاحبه (نکته طلایی)«به طور خلاصه، مهمترین نکتهای که در مصاحبهها باید به آن اشاره کنم این است که قرار نیست همه چیز را Non-blocking کنیم؛ بلکه هدف، اولویتبندی هوشمندانه است. ما میگوییم که تعاملات کاربر (User Interactions) همیشه اولویت بالایی دارند و هر عملیات سنگین دیگری باید به تعویق بیفتد تا برنامه همیشه چابک (Snappy) و زنده به نظر برسد.»یک دیاگرام ذهنی برای خودتان (برای حفظ کردن)مشکل: رندر سنگین = صفحه قفل میشود.راهحل: رندرینگ قابل وقفه (Concurrent).ابزار ۱ (useTransition): کنترل عملیات غیرفوری توسط خودمان.ابزار ۲ (useDeferredValue): دریافت مقدار با اولویت پایینتر از Props.نتیجه نهایی: کاربر میتواند تایپ/کلیک کند، درحالی که کار سنگین در پسزمینه داره انجام میشه.نکته اجرایی: هنگام گفتن این متن، حتماً روی کلمه &#34;اولویت&#34; و &#34;وقفه پذیری&#34; تأکید کنید. این دو واژه، کلید درک Concurrent Rendering هستند.۱. مثال کاربردی برای useTransition (جستجوی محصولات)سناریو: یک اپلیکیشن فروشگاهی که کاربر در باکس جستجو تایپ میکند و لیست محصولات فیلتر میشود.jsximport { useState, useTransition } from &#39;react&#39;;&#xA;&#xA;// تابع سنگین شبیهسازی فیلتر کردن لیست بزرگ (مثلاً ۱۰۰۰۰ آیتم)&#xA;function filterProducts(searchTerm) {&#xA;  const bigList = Array.from({ length: 10000 }, (_, i) =&gt; `Product ${i}`);&#xA;  // اگر کاربر چیزی تایپ نکرده باشد، همه را برگردان&#xA;  if (!searchTerm) return bigList;&#xA;  // شبیهسازی یک پردازش سنگین با فیلتر&#xA;  return bigList.filter(item =&gt; &#xA;    item.toLowerCase().includes(searchTerm.toLowerCase())&#xA;  );&#xA;}&#xA;&#xA;function SearchComponent() {&#xA;  const [keyword, setKeyword] = useState(&#39;&#39;);&#xA;  const [filteredList, setFilteredList] = useState([]);&#xA;  // ۱. صدا زدن هوک useTransition&#xA;  const [isPending, startTransition] = useTransition();&#xA;&#xA;  // تابعی که هنگام تایپ کاربر صدا زده میشود&#xA;  const handleSearch = (e) =&gt; {&#xA;    const newKeyword = e.target.value;&#xA;    setKeyword(newKeyword); // &lt;-- این آپدیت فوری است (برای اینپوت)&#xA;&#xA;    // ۲. شروع Transition&#xA;    startTransition(() =&gt; {&#xA;      // کار سنگین را داخل Transition قرار میدهیم&#xA;      const result = filterProducts(newKeyword);&#xA;      setFilteredList(result); // &lt;-- این آپدیت اولویت پایین دارد&#xA;    });&#xA;  };&#xA;&#xA;  return (&#xA;    &lt;div&gt;&#xA;      &lt;input &#xA;        type=&#34;text&#34; &#xA;        value={keyword} &#xA;        ={handleSearch} &#xA;        placeholder=&#34;جستجوی محصولات...&#34;&#xA;      /&gt;&#xA;      {/* ۳. نمایش وضعیت pending به کاربر */}&#xA;      {isPending &amp;&amp; &lt;p&gt;در حال جستجو...&lt;/p&gt;}&#xA;      &#xA;      &lt;ul&gt;&#xA;        {filteredList.map((item, index) =&gt; (&#xA;          &lt;li key={index}&gt;{item}&lt;/li&gt;&#xA;        ))}&#xA;      &lt;/ul&gt;&#xA;    &lt;/div&gt;&#xA;  );&#xA;}&#xA;&#xA;export default SearchComponent;توضیح شفاهی هنگام ارائه کد در مصاحبه:«در اینجا خط setKeyword خارج از Transition قرار دارد تا ورودی کاربر بیدرنگ آپدیت شود. ولی خط setFilteredList داخل Transition است. بنابراین اگر کاربر خیلی سریع تایپ کند، React کار قبلی فیلتر کردن را نیمهکاره رها میکند و به جای آن، روی تایپ جدید تمرکز میکند. isPending هم به ما کمک میکند تا یک اسپینر نشان دهیم و کاربر بداند که برنامه هنگ نکرده است.»۲. مثال کاربردی برای useDeferredValue (نمایش لیست بر اساس Props)سناریو: یک کامپوننت فرزند که یک props سنگین دریافت میکند و ما نمیخواهیم که والد، تابع آپدیت را در دست بگیرد (مثل استفاده در مسیریابی یا Redux).jsximport { useState, useDeferredValue, useMemo } from &#39;react&#39;;&#xA;&#xA;// تابع سنگین برای مرتبسازی یا فیلتر&#xA;function processLargeData(inputText) {&#xA;  const fakeData = Array.from({ length: 20000 }, (_, i) =&gt; `Item ${i}`);&#xA;  if (!inputText) return fakeData;&#xA;  return fakeData.filter(item =&gt; item.includes(inputText));&#xA;}&#xA;&#xA;// کامپوننت فرزند که داده را از والد میگیرد&#xA;function ListComponent({ searchTerm }) {&#xA;  // ۱. گرفتن یک نسخه با اولویت پایین از props&#xA;  const deferredSearchTerm = useDeferredValue(searchTerm);&#xA;  &#xA;  // ۲. استفاده از useMemo برای اینکه فقط وقتی مقدار deferred تغییر کرد، دوباره محاسبه کند&#xA;  const items = useMemo(() =&gt; {&#xA;    console.log(&#39;محاسبه سنگین با مقدار:&#39;, deferredSearchTerm);&#xA;    return processLargeData(deferredSearchTerm);&#xA;  }, [deferredSearchTerm]);&#xA;&#xA;  return (&#xA;    &lt;div&gt;&#xA;      &lt;p style={{ color: &#39;gray&#39; }}&gt;&#xA;        {/* اگر مقدار واقعی با مقدار deferred متفاوت است، یعنی یک آپدیت در حال انجام است */}&#xA;        {searchTerm !== deferredSearchTerm ? &#39;در حال بهروزرسانی...&#39; : &#39;نمایش دادهها&#39;}&#xA;      &lt;/p&gt;&#xA;      &lt;ul&gt;&#xA;        {items.slice(0, 50).map((item, index) =&gt; ( // فقط ۵۰ تا نشان بده&#xA;          &lt;li key={index}&gt;{item}&lt;/li&gt;&#xA;        ))}&#xA;      &lt;/ul&gt;&#xA;    &lt;/div&gt;&#xA;  );&#xA;}&#xA;&#xA;// کامپوننت والد&#xA;function ParentComponent() {&#xA;  const [inputValue, setInputValue] = useState(&#39;&#39;);&#xA;&#xA;  return (&#xA;    &lt;div&gt;&#xA;      &lt;h2&gt;جستجو (useDeferredValue)&lt;/h2&gt;&#xA;      &lt;input &#xA;        type=&#34;text&#34; &#xA;        value={inputValue}&#xA;        ={(e) =&gt; setInputValue(e.target.value)}&#xA;        placeholder=&#34;متن مورد نظر را وارد کنید...&#34;&#xA;      /&gt;&#xA;      {/* ارسال props به فرزند */}&#xA;      &lt;ListComponent searchTerm={inputValue} /&gt;&#xA;    &lt;/div&gt;&#xA;  );&#xA;}&#xA;&#xA;export default ParentComponent;توضیح شفاهی هنگام ارائه کد در مصاحبه:«در این مثال، والد اصلاً از Transition استفاده نمیکند. والد هر چه کاربر تایپ میکند، همان را به searchTerm پاس میدهد. اما در کامپوننت فرزند، با useDeferredValue یک کپی با تأخیر از آن مقدار میگیرم. نکته مهم این است که اگر کاربر سریع تایپ کند، کامپوننت چند بار با مقدار قدیمی رندر میشود تا زمانی که CPU آزاد شود و در نهایت مقدار جدید جایگزین گردد. این روش برای زمانی عالی است که ما کنترلگر Event Handler نیستیم (مثلاً داده از Redux یا Context میآید).»۳. مقایسه کنار هم (برای جمعبندی نهایی کدنویسی)اگر مصاحبهکننده پرسید &#34;کدام را کی استفاده کنم؟&#34;، این جدول ذهنی را توضیح دهید:موقعیتابزار مناسبدکمهای که عملیات سنگین انجام میدهد (مثل خروجی گرفتن گزارش)useTransitionسرچ باکس که میخواهید ورودی فوری باشد ولی نتایج با تأخیر بیایندuseTransitionداده از طریق props از بیرون میآید و شما کنترلی روی تابع آپدیت نداریدuseDeferredValueمیخواهید دقیقاً وضعیت loading را کنترل کنید (اسپینر دستی)useTransition (چون isPending دارد)یک نکته طلایی برای مصاحبه (با کد فرضی):اگر خواستند تفاوت را ثابت کنید، بگویید:«اگر کد مثال اول را با مثال دوم مقایسه کنید، در مثال اول setKeyword (آپدیت اینپوت) و setFilteredList (آپدیت لیست) هر دو در یک تابع هستند، اما با Transition از هم جدا شدند. ولی در مثال دوم، ما اصلاً تابع setter را اجرا نمیکنیم؛ React خودش تفاوت بین searchTerm فعلی و deferredSearchTerm را مدیریت میکند و رندرهای اضافی را انجام میدهد.»نکته اجرایی: حت</description><category>مصطفی رستگار</category><author>مصطفی رستگار</author><pubDate>Sun, 09 Aug 2026 14:39:01 +0330</pubDate></item><item><title>راهنمای جامع CRUD با Server Actions در Next.js 16 - نسخه معماری لایه ای</title><link>https://virgool.ir/@m.rastegar991/راهنمای-جامع-crud-با-server-actions-در-nextjs-16-نسخه-معماری-لایه-ای-nw9dqeqepx7k</link><description>فهرست مطالبمقدمهساختار پروژهلایه Utilsلایه Repositoryلایه Services (Server Actions)کامپوننتهای مستقلجمعبندیمقدمهدر این نسخه، کدها را به صورت لایهای و کاملاً جدا از هم سازماندهی میکنیم تا خوانایی، قابلیت نگهداری و تست پذیری افزایش یابد.معماری لایهای:text┌─────────────────────────────────────────────┐&#xA;│           Presentation Layer                │&#xA;│        (React Components)                   │&#xA;└──────────────────┬──────────────────────────┘&#xA;                   │&#xA;┌──────────────────▼──────────────────────────┐&#xA;│           Service Layer                     │&#xA;│        (Server Actions)                     │&#xA;└──────────────────┬──────────────────────────┘&#xA;                   │&#xA;┌──────────────────▼──────────────────────────┐&#xA;│           Repository Layer                  │&#xA;│        (API Communication)                  │&#xA;└──────────────────┬──────────────────────────┘&#xA;                   │&#xA;┌──────────────────▼──────────────────────────┐&#xA;│           Utils Layer                       │&#xA;│        (HTTP Client, Helpers)               │&#xA;└─────────────────────────────────────────────┘ساختار پروژهtextapp/&#xA;├── categories/&#xA;│   ├── page.tsx                         # صفحه اصلی&#xA;│   └── _components/&#xA;│       ├── CategoryList.tsx&#xA;│       ├── CategoryCreateForm.tsx&#xA;│       ├── CategoryEditForm.tsx&#xA;│       └── CategoryDeleteButton.tsx&#xA;├── lib/&#xA;│   ├── utils/&#xA;│   │   ├── http-client.ts               # کلاینت HTTP&#xA;│   │   └── token-manager.ts             # مدیریت توکن&#xA;│   ├── repositories/&#xA;│   │   └── category.repository.ts       # Repository دستهبندی&#xA;│   └── services/&#xA;│       └── category.service.ts          # Server Actions&#xA;└── types/&#xA;    └── category.ts                      # Typesلایه Utils1. مدیریت Tokentypescript// lib/utils/token-manager.ts&#xA;import { cookies } from &#39;next/headers&#39;;&#xA;&#xA;export class TokenManager {&#xA;  static async getAccessToken(): Promise&lt;string | undefined&gt; {&#xA;    const cookieStore = cookies();&#xA;    return cookieStore.get(&#39;access_token&#39;)?.value;&#xA;  }&#xA;&#xA;  static async getRefreshToken(): Promise&lt;string | undefined&gt; {&#xA;    const cookieStore = cookies();&#xA;    return cookieStore.get(&#39;refresh_token&#39;)?.value;&#xA;  }&#xA;&#xA;  static async getHeaders(): Promise&lt;HeadersInit&gt; {&#xA;    const token = await this.getAccessToken();&#xA;    &#xA;    return {&#xA;      &#39;Content-Type&#39;: &#39;application/json&#39;,&#xA;      ...(token &amp;&amp; { Authorization: `Bearer ${token}` }),&#xA;    };&#xA;  }&#xA;}2. HTTP Clienttypescript// lib/utils/http-client.ts&#xA;import { TokenManager } from &#39;./token-manager&#39;;&#xA;&#xA;export interface HttpClientOptions extends RequestInit {&#xA;  baseURL?: string;&#xA;}&#xA;&#xA;export interface ApiResponse&lt;T = any&gt; {&#xA;  success: boolean;&#xA;  data?: T;&#xA;  message?: string;&#xA;  errors?: Record&lt;string, string[]&gt;;&#xA;  statusCode?: number;&#xA;}&#xA;&#xA;export class HttpClient {&#xA;  private baseURL: string;&#xA;&#xA;  constructor(baseURL?: string) {&#xA;    this.baseURL = baseURL || process.env.API_BASE_URL || &#39;http://localhost:8000/api&#39;;&#xA;  }&#xA;&#xA;  private async request&lt;T&gt;(&#xA;    endpoint: string,&#xA;    options: HttpClientOptions = {}&#xA;  ): Promise&lt;ApiResponse&lt;T&gt;&gt; {&#xA;    const url = `${this.baseURL}${endpoint}`;&#xA;    const headers = await TokenManager.getHeaders();&#xA;&#xA;    try {&#xA;      const response = await fetch(url, {&#xA;        ...options,&#xA;        headers: {&#xA;          ...headers,&#xA;          ...options.headers,&#xA;        },&#xA;      });&#xA;&#xA;      const data = await response.json();&#xA;&#xA;      if (!response.ok) {&#xA;        return {&#xA;          success: false,&#xA;          message: data.message || &#39;An error occurred&#39;,&#xA;          errors: data.errors,&#xA;          statusCode: response.status,&#xA;        };&#xA;      }&#xA;&#xA;      return {&#xA;        success: true,&#xA;        data: data.data || data,&#xA;        message: data.message,&#xA;        statusCode: response.status,&#xA;      };&#xA;    } catch (error) {&#xA;      return {&#xA;        success: false,&#xA;        message: error instanceof Error ? error.message : &#39;Network error&#39;,&#xA;      };&#xA;    }&#xA;  }&#xA;&#xA;  // ============ HTTP Methods ============&#xA;  async get&lt;T&gt;(endpoint: string, options?: HttpClientOptions): Promise&lt;ApiResponse&lt;T&gt;&gt; {&#xA;    return this.request&lt;T&gt;(endpoint, { ...options, method: &#39;GET&#39; });&#xA;  }&#xA;&#xA;  async post&lt;T&gt;(endpoint: string, data?: any, options?: HttpClientOptions): Promise&lt;ApiResponse&lt;T&gt;&gt; {&#xA;    return this.request&lt;T&gt;(endpoint, {&#xA;      ...options,&#xA;      method: &#39;POST&#39;,&#xA;      body: data ? JSON.stringify(data) : undefined,&#xA;    });&#xA;  }&#xA;&#xA;  async put&lt;T&gt;(endpoint: string, data?: any, options?: HttpClientOptions): Promise&lt;ApiResponse&lt;T&gt;&gt; {&#xA;    return this.request&lt;T&gt;(endpoint, {&#xA;      ...options,&#xA;      method: &#39;PUT&#39;,&#xA;      body: data ? JSON.stringify(data) : undefined,&#xA;    });&#xA;  }&#xA;&#xA;  async patch&lt;T&gt;(endpoint: string, data?: any, options?: HttpClientOptions): Promise&lt;ApiResponse&lt;T&gt;&gt; {&#xA;    return this.request&lt;T&gt;(endpoint, {&#xA;      ...options,&#xA;      method: &#39;PATCH&#39;,&#xA;      body: data ? JSON.stringify(data) : undefined,&#xA;    });&#xA;  }&#xA;&#xA;  async delete&lt;T&gt;(endpoint: string, options?: HttpClientOptions): Promise&lt;ApiResponse&lt;T&gt;&gt; {&#xA;    return this.request&lt;T&gt;(endpoint, { ...options, method: &#39;DELETE&#39; });&#xA;  }&#xA;}&#xA;&#xA;// ============ Singleton Instance ============&#xA;let httpClientInstance: HttpClient | null = null;&#xA;&#xA;export function getHttpClient(): HttpClient {&#xA;  if (!httpClientInstance) {&#xA;    httpClientInstance = new HttpClient();&#xA;  }&#xA;  return httpClientInstance;&#xA;}لایه Repositorytypescript// lib/repositories/category.repository.ts&#xA;import { getHttpClient } from &#39;../utils/http-client&#39;;&#xA;import type { &#xA;  Category, &#xA;  CreateCategoryDto, &#xA;  UpdateCategoryDto,&#xA;  ApiResponse &#xA;} from &#39;@/types/category&#39;;&#xA;&#xA;export class CategoryRepository {&#xA;  private readonly endpoint = &#39;/categories&#39;;&#xA;&#xA;  // ============ CRUD Operations ============&#xA;  &#xA;  async getAll(): Promise&lt;ApiResponse&lt;Category[]&gt;&gt; {&#xA;    const http = getHttpClient();&#xA;    return http.get&lt;Category[]&gt;(this.endpoint);&#xA;  }&#xA;&#xA;  async getById(id: string): Promise&lt;ApiResponse&lt;Category&gt;&gt; {&#xA;    const http = getHttpClient();&#xA;    return http.get&lt;Category&gt;(`${this.endpoint}/${id}`);&#xA;  }&#xA;&#xA;  async create(data: CreateCategoryDto): Promise&lt;ApiResponse&lt;Category&gt;&gt; {&#xA;    const http = getHttpClient();&#xA;    return http.post&lt;Category&gt;(this.endpoint, data);&#xA;  }&#xA;&#xA;  async update(id: string, data: UpdateCategoryDto): Promise&lt;ApiResponse&lt;Category&gt;&gt; {&#xA;    const http = getHttpClient();&#xA;    return http.put&lt;Category&gt;(`${this.endpoint}/${id}`, data);&#xA;  }&#xA;&#xA;  async delete(id: string): Promise&lt;ApiResponse&lt;void&gt;&gt; {&#xA;    const http = getHttpClient();&#xA;    return http.delete&lt;void&gt;(`${this.endpoint}/${id}`);&#xA;  }&#xA;&#xA;  // ============ Additional Methods ============&#xA;  &#xA;  async getBySlug(slug: string): Promise&lt;ApiResponse&lt;Category&gt;&gt; {&#xA;    const http = getHttpClient();&#xA;    return http.get&lt;Category&gt;(`${this.endpoint}/slug/${slug}`);&#xA;  }&#xA;&#xA;  async search(query: string): Promise&lt;ApiResponse&lt;Category[]&gt;&gt; {&#xA;    const http = getHttpClient();&#xA;    return http.get&lt;Category[]&gt;(`${this.endpoint}/search?q=${encodeURIComponent(query)}`);&#xA;  }&#xA;}&#xA;&#xA;// ============ Singleton Instance ============&#xA;let categoryRepositoryInstance: CategoryRepository | null = null;&#xA;&#xA;export function getCategoryRepository(): CategoryRepository {&#xA;  if (!categoryRepositoryInstance) {&#xA;    categoryRepositoryInstance = new CategoryRepository();&#xA;  }&#xA;  return categoryRepositoryInstance;&#xA;}لایه Services (Server Actions)typescript// lib/services/category.service.ts&#xA;&#39;use server&#39;;&#xA;&#xA;import { revalidateTag } from &#39;next/cache&#39;;&#xA;import { getCategoryRepository } from &#39;../repositories/category.repository&#39;;&#xA;import type { &#xA;  Category, &#xA;  CreateCategoryDto, &#xA;  UpdateCategoryDto,&#xA;  FormState &#xA;} from &#39;@/types/category&#39;;&#xA;&#xA;// ============ Query Services ============&#xA;&#xA;export async function getCategories(): Promise&lt;FormState&lt;Category[]&gt;&gt; {&#xA;  &#39;use server&#39;;&#xA;  &#xA;  try {&#xA;    const repository = getCategoryRepository();&#xA;    const result = await repository.getAll();&#xA;    &#xA;    if (!result.success) {&#xA;      return {&#xA;        success: false,&#xA;        message: result.message,&#xA;        errors: result.errors,&#xA;      };&#xA;    }&#xA;&#xA;    return {&#xA;      success: true,&#xA;      data: result.data || [],&#xA;    };&#xA;  } catch (error) {&#xA;    return {&#xA;      success: false,&#xA;      message: &#39;Failed to fetch categories&#39;,&#xA;    };&#xA;  }&#xA;}&#xA;&#xA;export async function getCategoryById(id: string): Promise&lt;FormState&lt;Category&gt;&gt; {&#xA;  &#39;use server&#39;;&#xA;  &#xA;  try {&#xA;    const repository = getCategoryRepository();&#xA;    const result = await repository.getById(id);&#xA;    &#xA;    if (!result.success) {&#xA;      return {&#xA;        success: false,&#xA;        message: result.message,&#xA;        errors: result.errors,&#xA;      };&#xA;    }&#xA;&#xA;    return {&#xA;      success: true,&#xA;      data: result.data,&#xA;    };&#xA;  } catch (error) {&#xA;    return {&#xA;      success: false,&#xA;      message: &#39;Failed to fetch category&#39;,&#xA;    };&#xA;  }&#xA;}&#xA;&#xA;// ============ Mutation Services ============&#xA;&#xA;export async function createCategory(&#xA;  prevState: FormState,&#xA;  formData: FormData&#xA;): Promise&lt;FormState&lt;Category&gt;&gt; {&#xA;  &#39;use server&#39;;&#xA;&#xA;  // Extract data&#xA;  const data: CreateCategoryDto = {&#xA;    name: formData.get(&#39;name&#39;) as string,&#xA;    description: formData.get(&#39;description&#39;) as string,&#xA;  };&#xA;&#xA;  // Client-side validation (optional)&#xA;  if (!data.name?.trim()) {&#xA;    return {&#xA;      success: false,&#xA;      errors: { name: [&#39;Name is required&#39;] },&#xA;    };&#xA;  }&#xA;&#xA;  try {&#xA;    const repository = getCategoryRepository();&#xA;    const result = await repository.create(data);&#xA;&#xA;    if (!result.success) {&#xA;      return {&#xA;        success: false,&#xA;        message: result.message,&#xA;        errors: result.errors,&#xA;      };&#xA;    }&#xA;&#xA;    // Revalidate cache&#xA;    revalidateTag(&#39;categories&#39;);&#xA;    if (result.data) {&#xA;      revalidateTag(`category_${result.data.id}`);&#xA;    }&#xA;&#xA;    return {&#xA;      success: true,&#xA;      data: result.data,&#xA;      message: &#39;Category created successfully&#39;,&#xA;    };&#xA;  } catch (error) {&#xA;    return {&#xA;      success: false,&#xA;      message: &#39;Failed to create category&#39;,&#xA;    };&#xA;  }&#xA;}&#xA;&#xA;export async function updateCategory(&#xA;  id: string,&#xA;  prevState: FormState,&#xA;  formData: FormData&#xA;): Promise&lt;FormState&lt;Category&gt;&gt; {&#xA;  &#39;use server&#39;;&#xA;&#xA;  // Extract data&#xA;  const data: UpdateCategoryDto = {&#xA;    id,&#xA;    name: formData.get(&#39;name&#39;) as string,&#xA;    description: formData.get(&#39;description&#39;) as string,&#xA;  };&#xA;&#xA;  try {&#xA;    const repository = getCategoryRepository();&#xA;    const result = await repository.update(id, data);&#xA;&#xA;    if (!result.success) {&#xA;      return {&#xA;        success: false,&#xA;        message: result.message,&#xA;        errors: result.errors,&#xA;      };&#xA;    }&#xA;&#xA;    // Revalidate cache&#xA;    revalidateTag(&#39;categories&#39;);&#xA;    revalidateTag(`category_${id}`);&#xA;&#xA;    return {&#xA;      success: true,&#xA;      data: result.data,&#xA;      message: &#39;Category updated successfully&#39;,&#xA;    };&#xA;  } catch (error) {&#xA;    return {&#xA;      success: false,&#xA;      message: &#39;Failed to update category&#39;,&#xA;    };&#xA;  }&#xA;}&#xA;&#xA;export async function deleteCategory(&#xA;  id: string,&#xA;  prevState: FormState&#xA;): Promise&lt;FormState&gt; {&#xA;  &#39;use server&#39;;&#xA;&#xA;  try {&#xA;    const repository = getCategoryRepository();&#xA;    const result = await repository.delete(id);&#xA;&#xA;    if (!result.success) {&#xA;      return {&#xA;        success: false,&#xA;        message: result.message,&#xA;      };&#xA;    }&#xA;&#xA;    // Revalidate cache&#xA;    revalidateTag(&#39;categories&#39;);&#xA;    revalidateTag(`category_${id}`);&#xA;&#xA;    return {&#xA;      success: true,&#xA;      message: &#39;Category deleted successfully&#39;,&#xA;    };&#xA;  } catch (error) {&#xA;    return {&#xA;      success: false,&#xA;      message: &#39;Failed to delete category&#39;,&#xA;    };&#xA;  }&#xA;}&#xA;&#xA;// ============ Bulk Operations ============&#xA;&#xA;export async function deleteMultipleCategories(&#xA;  ids: string[]&#xA;): Promise&lt;FormState&gt; {&#xA;  &#39;use server&#39;;&#xA;&#xA;  try {&#xA;    const repository = getCategoryRepository();&#xA;    const results = await Promise.all(&#xA;      ids.map(id =&gt; repository.delete(id))&#xA;    );&#xA;&#xA;    const allSuccess = results.every(r =&gt; r.success);&#xA;&#xA;    if (!allSuccess) {&#xA;      return {&#xA;        success: false,&#xA;        message: &#39;Failed to delete some categories&#39;,&#xA;      };&#xA;    }&#xA;&#xA;    revalidateTag(&#39;categories&#39;);&#xA;    ids.forEach(id =&gt; revalidateTag(`category_${id}`));&#xA;&#xA;    return {&#xA;      success: true,&#xA;      message: &#39;Categories deleted successfully&#39;,&#xA;    };&#xA;  } catch (error) {&#xA;    return {&#xA;      success: false,&#xA;      message: &#39;Failed to delete categories&#39;,&#xA;    };&#xA;  }&#xA;}کامپوننت های مستقل1. کامپوننت Createtsx// app/categories/_components/CategoryCreateForm.tsx&#xA;&#39;use client&#39;;&#xA;&#xA;import { useActionState, useEffect, useRef } from &#39;react&#39;;&#xA;import { useRouter } from &#39;next/navigation&#39;;&#xA;import { createCategory } from &#39;@/lib/services/category.service&#39;;&#xA;import type { FormState } from &#39;@/types/category&#39;;&#xA;&#xA;interface CategoryCreateFormProps {&#xA;  onSuccess?: () =&gt; void;&#xA;  ?: () =&gt; void;&#xA;}&#xA;&#xA;export function CategoryCreateForm({ onSuccess,  }: CategoryCreateFormProps) {&#xA;  const router = useRouter();&#xA;  const formRef = useRef&lt;HTMLFormElement&gt;(null);&#xA;&#xA;  const [state, formAction, isPending] = useActionState(&#xA;    createCategory,&#xA;    {} as FormState&#xA;  );&#xA;&#xA;  useEffect(() =&gt; {&#xA;    if (state.success) {&#xA;      formRef.current?.reset();&#xA;      onSuccess?.();&#xA;      router.refresh();&#xA;    }&#xA;  }, [state.success, onSuccess, router]);&#xA;&#xA;  return (&#xA;    &lt;form ref={formRef} action={formAction} className=&#34;space-y-4&#34;&gt;&#xA;      {state.message &amp;&amp; !state.success &amp;&amp; (&#xA;        &lt;div className=&#34;p-3 bg-red-50 border border-red-200 rounded text-red-700 text-sm&#34;&gt;&#xA;          {state.message}&#xA;        &lt;/div&gt;&#xA;      )}&#xA;&#xA;      {state.success &amp;&amp; (&#xA;        &lt;div className=&#34;p-3 bg-green-50 border border-green-200 rounded text-green-700 text-sm&#34;&gt;&#xA;          {state.message}&#xA;        &lt;/div&gt;&#xA;      )}&#xA;&#xA;      &lt;div&gt;&#xA;        &lt;label htmlFor=&#34;create-name&#34; className=&#34;block text-sm font-medium text-gray-700&#34;&gt;&#xA;          Name &lt;span className=&#34;text-red-500&#34;&gt;*&lt;/span&gt;&#xA;        &lt;/label&gt;&#xA;        &lt;input&#xA;          type=&#34;text&#34;&#xA;          id=&#34;create-name&#34;&#xA;          name=&#34;name&#34;&#xA;          className=&#34;mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500&#34;&#xA;          placeholder=&#34;Enter category name&#34;&#xA;          disabled={isPending}&#xA;        /&gt;&#xA;        {state.errors?.name &amp;&amp; (&#xA;          &lt;p className=&#34;mt-1 text-sm text-red-600&#34;&gt;&#xA;            {state.errors.name.join(&#39;, &#39;)}&#xA;          &lt;/p&gt;&#xA;        )}&#xA;      &lt;/div&gt;&#xA;&#xA;      &lt;div&gt;&#xA;        &lt;label htmlFor=&#34;create-description&#34; className=&#34;block text-sm font-medium text-gray-700&#34;&gt;&#xA;          Description&#xA;        &lt;/label&gt;&#xA;        &lt;textarea&#xA;          id=&#34;create-description&#34;&#xA;          name=&#34;description&#34;&#xA;          rows={3}&#xA;          className=&#34;mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500&#34;&#xA;          placeholder=&#34;Enter category description&#34;&#xA;          disabled={isPending}&#xA;        /&gt;&#xA;        {state.errors?.description &amp;&amp; (&#xA;          &lt;p className=&#34;mt-1 text-sm text-red-600&#34;&gt;&#xA;            {state.errors.description.join(&#39;, &#39;)}&#xA;          &lt;/p&gt;&#xA;        )}&#xA;      &lt;/div&gt;&#xA;&#xA;      &lt;div className=&#34;flex gap-3 pt-2&#34;&gt;&#xA;        &lt;button&#xA;          type=&#34;submit&#34;&#xA;          disabled={isPending}&#xA;          className=&#34;flex-1 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors&#34;&#xA;        &gt;&#xA;          {isPending ? (&#xA;            &lt;&gt;&#xA;              &lt;span className=&#34;inline-block animate-spin mr-2&#34;&gt;⏳&lt;/span&gt;&#xA;              Creating...&#xA;            &lt;/&gt;&#xA;          ) : (&#xA;            &#39;Create Category&#39;&#xA;          )}&#xA;        &lt;/button&gt;&#xA;&#xA;        { &amp;&amp; (&#xA;          &lt;button&#xA;            type=&#34;button&#34;&#xA;            ={}&#xA;            disabled={isPending}&#xA;            className=&#34;px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 disabled:opacity-50 transition-colors&#34;&#xA;          &gt;&#xA;            Cancel&#xA;          &lt;/button&gt;&#xA;        )}&#xA;      &lt;/div&gt;&#xA;    &lt;/form&gt;&#xA;  );&#xA;}2. کامپوننت Edittsx// app/categories/_components/CategoryEditForm.tsx&#xA;&#39;use client&#39;;&#xA;&#xA;import { useActionState, useEffect } from &#39;react&#39;;&#xA;import { useRouter } from &#39;next/navigation&#39;;&#xA;import { updateCategory } from &#39;@/lib/services/category.service&#39;;&#xA;import type { Category, FormState } from &#39;@/types/category&#39;;&#xA;&#xA;interface CategoryEditFormProps {&#xA;  category: Category;&#xA;  onSuccess?: () =&gt; void;&#xA;}&#xA;&#xA;export function CategoryEditForm({ category, onSuccess }: CategoryEditFormProps) {&#xA;  const router = useRouter();&#xA;&#xA;  const [state, formAction, isPending] = useActionState(&#xA;    updateCategory.bind(null, category.id),&#xA;    {} as FormState&lt;Category&gt;&#xA;  );&#xA;&#xA;  useEffect(() =&gt; {&#xA;    if (state.success) {&#xA;      onSuccess?.();&#xA;      router.refresh();&#xA;    }&#xA;  }, [state.success, onSuccess, router]);&#xA;&#xA;  return (&#xA;    &lt;form action={formAction} className=&#34;space-y-4&#34;&gt;&#xA;      {state.message &amp;&amp; !state.success &amp;&amp; (&#xA;        &lt;div className=&#34;p-3 bg-red-50 border border-red-200 rounded text-red-700 text-sm&#34;&gt;&#xA;          {state.message}&#xA;        &lt;/div&gt;&#xA;      )}&#xA;&#xA;      {state.success &amp;&amp; (&#xA;        &lt;div className=&#34;p-3 bg-green-50 border border-green-200 rounded text-green-700 text-sm&#34;&gt;&#xA;          {state.message}&#xA;        &lt;/div&gt;&#xA;      )}&#xA;&#xA;      &lt;div&gt;&#xA;        &lt;label htmlFor=&#34;edit-name&#34; className=&#34;block text-sm font-medium text-gray-700&#34;&gt;&#xA;          Name &lt;span className=&#34;text-red-500&#34;&gt;*&lt;/span&gt;&#xA;        &lt;/label&gt;&#xA;        &lt;input&#xA;          type=&#34;text&#34;&#xA;          id=&#34;edit-name&#34;&#xA;          name=&#34;name&#34;&#xA;          defaultValue={category.name}&#xA;          className=&#34;mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500&#34;&#xA;          placeholder=&#34;Enter category name&#34;&#xA;          disabled={isPending}&#xA;        /&gt;&#xA;        {state.errors?.name &amp;&amp; (&#xA;          &lt;p className=&#34;mt-1 text-sm text-red-600&#34;&gt;&#xA;            {state.errors.name.join(&#39;, &#39;)}&#xA;          &lt;/p&gt;&#xA;        )}&#xA;      &lt;/div&gt;&#xA;&#xA;      &lt;div&gt;&#xA;        &lt;label htmlFor=&#34;edit-description&#34; className=&#34;block text-sm font-medium text-gray-700&#34;&gt;&#xA;          Description&#xA;        &lt;/label&gt;&#xA;        &lt;textarea&#xA;          id=&#34;edit-description&#34;&#xA;          name=&#34;description&#34;&#xA;          rows={3}&#xA;          defaultValue={category.description}&#xA;          className=&#34;mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500&#34;&#xA;          placeholder=&#34;Enter category description&#34;&#xA;          disabled={isPending}&#xA;        /&gt;&#xA;        {state.errors?.description &amp;&amp; (&#xA;          &lt;p className=&#34;mt-1 text-sm text-red-600&#34;&gt;&#xA;            {state.errors.description.join(&#39;, &#39;)}&#xA;          &lt;/p&gt;&#xA;        )}&#xA;      &lt;/div&gt;&#xA;&#xA;      &lt;div className=&#34;flex gap-3 pt-2&#34;&gt;&#xA;        &lt;button&#xA;          type=&#34;submit&#34;&#xA;          disabled={isPending}&#xA;          className=&#34;flex-1 px-4 py-2 bg-yellow-600 text-white rounded-md hover:bg-yellow-700 focus:outline-none focus:ring-2 focus:ring-yellow-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors&#34;&#xA;        &gt;&#xA;          {isPending ? (&#xA;            &lt;&gt;&#xA;              &lt;span className=&#34;inline-block animate-spin mr-2&#34;&gt;⏳&lt;/span&gt;&#xA;              Updating...&#xA;            &lt;/&gt;&#xA;          ) : (&#xA;            &#39;Update Category&#39;&#xA;          )}&#xA;        &lt;/button&gt;&#xA;&#xA;        { &amp;&amp; (&#xA;          &lt;button&#xA;            type=&#34;button&#34;&#xA;            disabled={isPending}&#xA;            className=&#34;px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 disabled:opacity-50 transition-colors&#34;&#xA;          &gt;&#xA;            Cancel&#xA;          &lt;/button&gt;&#xA;        )}&#xA;      &lt;/div&gt;&#xA;    &lt;/form&gt;&#xA;  );&#xA;}3. کامپوننت Deletetsx// app/categories/_components/CategoryDeleteButton.tsx&#xA;&#39;use client&#39;;&#xA;&#xA;import { useActionState, useEffect, useState } from &#39;react&#39;;&#xA;import { useRouter } from &#39;next/navigation&#39;;&#xA;import { deleteCategory } from &#39;@/lib/services/category.service&#39;;&#xA;import type { FormState } from &#39;@/types/category&#39;;&#xA;&#xA;interface CategoryDeleteButtonProps {&#xA;  categoryId: string;&#xA;  categoryName: string;&#xA;  onDeleted?: () =&gt; void;&#xA;}&#xA;&#xA;export function CategoryDeleteButton({&#xA;  categoryId,&#xA;  categoryName,&#xA;  onDeleted,&#xA;}: CategoryDeleteButtonProps) {&#xA;  const router = useRouter();&#xA;  const [isModalOpen, setIsModalOpen] = useState(false);&#xA;&#xA;  const [state, formAction, isPending] = useActionState(&#xA;    deleteCategory.bind(null, categoryId),&#xA;    {} as FormState&#xA;  );&#xA;&#xA;  useEffect(() =&gt; {&#xA;    if (state.success) {&#xA;      setIsModalOpen(false);&#xA;      onDeleted?.();&#xA;      router.refresh();&#xA;    }&#xA;  }, [state.success, onDeleted, router]);&#xA;&#xA;  return (&#xA;    &lt;&gt;&#xA;      &lt;button&#xA;        ={() =&gt; setIsModalOpen(true)}&#xA;        className=&#34;px-3 py-1 text-sm bg-red-100 text-red-700 rounded hover:bg-red-200 transition-colors&#34;&#xA;      &gt;&#xA;        🗑️ Delete&#xA;      &lt;/button&gt;&#xA;&#xA;      {isModalOpen &amp;&amp; (&#xA;        &lt;div className=&#34;fixed inset-0 z-50 flex items-center justify-center bg-black/50&#34;&gt;&#xA;          &lt;div className=&#34;bg-white rounded-lg p-6 max-w-md w-full mx-4&#34;&gt;&#xA;            &lt;h3 className=&#34;text-lg font-semibold text-gray-900 mb-2&#34;&gt;&#xA;              Delete Category&#xA;            &lt;/h3&gt;&#xA;            &#xA;            &lt;p className=&#34;text-gray-600 mb-4&#34;&gt;&#xA;              Are you sure you want to delete &lt;strong&gt;&#34;{categoryName}&#34;&lt;/strong&gt;?&#xA;              This action cannot be undone.&#xA;            &lt;/p&gt;&#xA;&#xA;            {state.message &amp;&amp; !state.success &amp;&amp; (&#xA;              &lt;div className=&#34;mb-4 p-3 bg-red-50 border border-red-200 rounded text-red-700 text-sm&#34;&gt;&#xA;                {state.message}&#xA;              &lt;/div&gt;&#xA;            )}&#xA;&#xA;            &lt;form action={formAction} className=&#34;flex gap-3&#34;&gt;&#xA;              &lt;button&#xA;                type=&#34;submit&#34;&#xA;                disabled={isPending}&#xA;                className=&#34;flex-1 px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors&#34;&#xA;              &gt;&#xA;                {isPending ? (&#xA;                  &lt;&gt;&#xA;                    &lt;span className=&#34;inline-block animate-spin mr-2&#34;&gt;⏳&lt;/span&gt;&#xA;                    Deleting...&#xA;                  &lt;/&gt;&#xA;                ) : (&#xA;                  &#39;Yes, Delete&#39;&#xA;                )}&#xA;              &lt;/button&gt;&#xA;&#xA;              &lt;button&#xA;                type=&#34;button&#34;&#xA;                ={() =&gt; setIsModalOpen(false)}&#xA;                disabled={isPending}&#xA;                className=&#34;flex-1 px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 disabled:opacity-50 transition-colors&#34;&#xA;              &gt;&#xA;                Cancel&#xA;              &lt;/button&gt;&#xA;            &lt;/form&gt;&#xA;          &lt;/div&gt;&#xA;        &lt;/div&gt;&#xA;      )}&#xA;    &lt;/&gt;&#xA;  );&#xA;}4. کامپوننت Listtsx// app/categories/_components/CategoryList.tsx&#xA;&#39;use client&#39;;&#xA;&#xA;import { useState } from &#39;react&#39;;&#xA;import { CategoryCreateForm } from &#39;./CategoryCreateForm&#39;;&#xA;import { CategoryEditForm } from &#39;./CategoryEditForm&#39;;&#xA;import { CategoryDeleteButton } from &#39;./CategoryDeleteButton&#39;;&#xA;import type { Category } from &#39;@/types/category&#39;;&#xA;&#xA;interface CategoryListProps {&#xA;  initialData: Category[];&#xA;}&#xA;&#xA;export function CategoryList({ initialData }: CategoryListProps) {&#xA;  const [categories] = useState&lt;Category[]&gt;(initialData);&#xA;  const [editingCategory, setEditingCategory] = useState&lt;Category | null&gt;(null);&#xA;  const [showCreateForm, setShowCreateForm] = useState(false);&#xA;&#xA;  const handleDataChange = () =&gt; {&#xA;    setShowCreateForm(false);&#xA;    setEditingCategory(null);&#xA;  };&#xA;&#xA;  return (&#xA;    &lt;div className=&#34;space-y-6&#34;&gt;&#xA;      {/* Header */}&#xA;      &lt;div className=&#34;flex justify-between items-center&#34;&gt;&#xA;        &lt;h2 className=&#34;text-xl font-semibold&#34;&gt;Categories&lt;/h2&gt;&#xA;        &lt;button&#xA;          ={() =&gt; setShowCreateForm(true)}&#xA;          className=&#34;px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 transition-colors&#34;&#xA;        &gt;&#xA;          + New Category&#xA;        &lt;/button&gt;&#xA;      &lt;/div&gt;&#xA;&#xA;      {/* Create Form */}&#xA;      {showCreateForm &amp;&amp; (&#xA;        &lt;div className=&#34;p-4 border border-gray-200 rounded-lg bg-gray-50&#34;&gt;&#xA;          &lt;h3 className=&#34;text-lg font-medium mb-4&#34;&gt;Create New Category&lt;/h3&gt;&#xA;          &lt;CategoryCreateForm&#xA;            onSuccess={handleDataChange}&#xA;            ={() =&gt; setShowCreateForm(false)}&#xA;          /&gt;&#xA;        &lt;/div&gt;&#xA;      )}&#xA;&#xA;      {/* Edit Form */}&#xA;      {editingCategory &amp;&amp; (&#xA;        &lt;div className=&#34;p-4 border border-gray-200 rounded-lg bg-yellow-50&#34;&gt;&#xA;          &lt;h3 className=&#34;text-lg font-medium mb-4&#34;&gt;Edit Category&lt;/h3&gt;&#xA;          &lt;CategoryEditForm&#xA;            category={editingCategory}&#xA;            onSuccess={handleDataChange}&#xA;            ={() =&gt; setEditingCategory(null)}&#xA;          /&gt;&#xA;        &lt;/div&gt;&#xA;      )}&#xA;&#xA;      {/* List */}&#xA;      &lt;div className=&#34;bg-white border border-gray-200 rounded-lg overflow-hidden&#34;&gt;&#xA;        {categories.length === 0 ? (&#xA;          &lt;div className=&#34;p-8 text-center text-gray-500&#34;&gt;&#xA;            No categories found. Create your first category!&#xA;          &lt;/div&gt;&#xA;        ) : (&#xA;          &lt;div className=&#34;divide-y divide-gray-200&#34;&gt;&#xA;            {categories.map((category) =&gt; (&#xA;              &lt;div&#xA;                key={category.id}&#xA;                className=&#34;flex items-center justify-between p-4 hover:bg-gray-50 transition-colors&#34;&#xA;              &gt;&#xA;                &lt;div className=&#34;flex-1 min-w-0&#34;&gt;&#xA;                  &lt;h3 className=&#34;text-sm font-medium text-gray-900 truncate&#34;&gt;&#xA;                    {category.name}&#xA;                  &lt;/h3&gt;&#xA;                  {category.description &amp;&amp; (&#xA;                    &lt;p className=&#34;text-sm text-gray-500 truncate&#34;&gt;&#xA;                      {category.description}&#xA;                    &lt;/p&gt;&#xA;                  )}&#xA;                  &lt;p className=&#34;text-xs text-gray-400 mt-1&#34;&gt;&#xA;                    Created: {new Date(category.createdAt).toLocaleDateString()}&#xA;                  &lt;/p&gt;&#xA;                &lt;/div&gt;&#xA;&#xA;                &lt;div className=&#34;ml-4 flex-shrink-0 flex gap-2&#34;&gt;&#xA;                  &lt;button&#xA;                    ={() =&gt; setEditingCategory(category)}&#xA;                    className=&#34;px-3 py-1 text-sm bg-blue-100 text-blue-700 rounded hover:bg-blue-200 transition-colors&#34;&#xA;                  &gt;&#xA;                    ✏️ Edit&#xA;                  &lt;/button&gt;&#xA;&#xA;                  &lt;CategoryDeleteButton&#xA;                    categoryId={category.id}&#xA;                    categoryName={category.name}&#xA;                    onDeleted={handleDataChange}&#xA;                  /&gt;&#xA;                &lt;/div&gt;&#xA;              &lt;/div&gt;&#xA;            ))}&#xA;          &lt;/div&gt;&#xA;        )}&#xA;      &lt;/div&gt;&#xA;    &lt;/div&gt;&#xA;  );&#xA;}صفحه اصلیtsx// app/categories/page.tsx&#xA;import { cacheLife, cacheTag } from &#39;next/cache&#39;;&#xA;import { Suspense } from &#39;react&#39;;&#xA;import { CategoryList } from &#39;./_components/CategoryList&#39;;&#xA;import { CategorySkeleton } from &#39;./_components/CategorySkeleton&#39;;&#xA;import { getCategories } from &#39;@/lib/services/category.service&#39;;&#xA;&#xA;// تابع دریافت داده با کش&#xA;async function fetchCategories() {&#xA;  &#39;use cache&#39;;&#xA;  cacheLife(&#39;minutes&#39;);&#xA;  cacheTag(&#39;categories&#39;);&#xA;&#xA;  const result = await getCategories();&#xA;&#xA;  if (!result.success) {&#xA;    throw new Error(result.message || &#39;Failed to load categories&#39;);&#xA;  }&#xA;&#xA;  return result.data || [];&#xA;}&#xA;&#xA;// کامپوننت Async&#xA;async function CategoriesContent() {&#xA;  const categories = await fetchCategories();&#xA;  return &lt;CategoryList initialData={categories} /&gt;;&#xA;}&#xA;&#xA;// صفحه اصلی&#xA;export default function CategoriesPage() {&#xA;  return (&#xA;    &lt;div className=&#34;container mx-auto p-4 max-w-4xl&#34;&gt;&#xA;      &lt;h1 className=&#34;text-3xl font-bold mb-6&#34;&gt;Category Management&lt;/h1&gt;&#xA;&#xA;      &lt;Suspense fallback={&lt;CategorySkeleton /&gt;}&gt;&#xA;        &lt;CategoriesContent /&gt;&#xA;      &lt;/Suspense&gt;&#xA;    &lt;/div&gt;&#xA;  );&#xA;}جمعبندیمزایای معماری لایه ای:✅ Separation of Concerns: هر لایه مسئولیت مشخصی دارد✅ خوانایی بالا: کدها سازماندهی شده و قابل فهم هستند✅ قابلیت نگهداری: تغییر در هر لایه بدون تأثیر بر لایه های دیگر✅ تستپذیری: هر لایه به راحتی قابل تست است✅ Reusability: کلاس ها و توابع در جای دیگر قابل استفاده✅ Dependency Injection: استفاده از Singleton Pattern✅ Type Safety: استفاده از TypeScript در تمام لایه هاساختار لایه ها:بهترین Practices:استفاده از Singleton Pattern برای کلاس هااستفاده از &#39;use server&#39; در لایه Serviceاستفاده از revalidateTag برای مدیریت کشاستفاده از useActionState در کامپوننت هامدیریت خطا در هر لایه</description><category>مصطفی رستگار</category><author>مصطفی رستگار</author><pubDate>Sun, 09 Aug 2026 14:35:46 +0330</pubDate></item><item><title>21- React Fiber معماری (تقسیم کار و Concurrent Mode)</title><link>https://virgool.ir/@m.rastegar991/21-react-fiber-معماری-تقسیم-کار-و-concurrent-mode-sf0lkxr1kpfo</link><description>متن آماده برای مصاحبه (بخش اول: معماری و تقسیم کار)«برای درک ری‌اکت فایبر، بهتر است اول ببینیم که ری‌اکت قدیم (Stack) چه مشکلی داشت. قبلاً وقتی وضعیت یک کامپوننت تغییر می‌کرد، ری‌اکت یک کارخانه بود که تمام مراحل ساخت را یک‌جا و پشت سر هم انجام می‌داد. یعنی می‌گفت: «برو، همه جا را بگرد، هر تغییری هست را محاسبه کن، بلافاصله در DOM واقعی اعمال کن و تمام!» اگر این محاسبات طول می‌کشید، مرورگر یخ می‌زد و کاربر کلیک کردن یا تایپ کردن را احساس نمی‌کرد.اما فایبر، معماری جدید ری‌اکت، این کارخانه را به یک تیم مدیریت پروژه تبدیل کرد. در اینجا، کار به واحدهای کوچک‌تری به نام «یونیت‌های کاری» یا Fiber nodes تقسیم می‌شود. به جای اینکه یک‌باره همه کارها را انجام دهد، فایبر این کارها را اولویت‌بندی می‌کند.مهم‌ترین نکته این است که فایبر دو فاز مجزا دارد:فاز Render (هماهنگی): در این مرحله، ری‌اکت یک «درخت مجازی» از تغییرات می‌سازد و محاسبه می‌کند که چه چیزی باید عوض شود. اما هنوز هیچ چیزی را در صفحه واقعی نشان نمی‌دهد. این فاز قابل وقفه است.فاز Commit (اعمال): این فاز کوتاه و غیرقابل‌وقفه است. یعنی ری‌اکت تغییرات نهایی را برمی‌دارد و یک‌جا به DOM واقعی می‌زند تا صفحه به‌روز شود.»متن آماده برای مصاحبه (بخش دوم: Concurrent Mode و تفاوت آن)«حالا این معماری به ما اجازه می‌دهد که ویژگی فوق‌العاده‌ای به نام Concurrent Mode یا «حالت هم‌روند» داشته باشیم. کلمه Concurrent به معنی «هم‌زمان» نیست؛ بلکه به معنی «تداخل در اولویت‌ها» است.فرض کنید در حال تایپ کردن در یک فیلد جستجو هستیم و همزمان، لیست عظیمی از نتایج باید فیلتر شود. در حالت عادی، تایپ کردن ما کند می‌شود چون ری‌اکت مشغول رندر کردن لیست است.در Concurrent Mode، ری‌اکت مثل یک منشی هوشمند عمل می‌کند. وقتی من دارم تایپ می‌کنم، فایبر به رندرینگ سنگین لیست می‌گوید: «صبر کن، فعلاً اولویت با کاربر است، کارت را متوقف کن و به من بده تا کی‌برد را پردازش کنم.» بعد که تایپ من تمام شد، دوباره کار رندر لیست را از همان جایی که متوقف کرده بود، ادامه می‌دهد.به این قابلیت، زمان‌بندی (Scheduling) می‌گوییم. یعنی ری‌اکت خودش تصمیم می‌گیرد کدام آپدیت اورژانسی‌تر است (مثل ورودی کاربر) و کدام آپدیت را می‌تواند به تعویق بیندازد (مثل رندر کردن لیست‌های سنگین).»جمع‌بندی نهایی برای مصاحبه‌کننده«در کل، فایبر به ری‌اکت یک موتور هوشمند داده که دیگر کورکورانه همه چیز را دوباره نمی‌سازد. بلکه کارها را به قطعات کوچک تقسیم می‌کند، اولویت‌بندی می‌کند و در صورت لزوم، کار سنگین را متوقف کرده تا تجربه کاربری نرم‌تر و روان‌تری داشته باشیم. این معماری پایه‌ای برای قابلیت‌های مدرن Next.js مانند Server Components هم هست، چون به ری‌اکت اجازه می‌دهد که منتظر داده‌های سرور بماند بدون اینکه کل صفحه را مسدود کند.»نکات طلایی برای بیان آن در مصاحبه:از کلمه «وقفه» استفاده کنید: بگویید «قابل وقفه» بودن، مهم‌ترین تفاوت فایبر با معماری قبلی است.تفاوت Concurrent با Parallel را بدانید: اگر مصاحبه‌کننده پرسید، بگویید Parallel یعنی همزمان اجرا شدن روی چند هسته (که در جاوااسکریپت تک‌رشته‌ای نداریم)، ولی Concurrent یعنی مدیریت تداخل و اولویت‌بندی چند کار در یک زمان.به Next.js ربط دهید: اگر از Next.js 13 یا 14 استفاده می‌کنید، اضافه کنید که Suspense و Streaming در Next.js دقیقاً از همین قابلیت فایبر برای منتظر ماندن و ادامه‌دادن استفاده می‌کنند.</description><category>مصطفی رستگار</category><author>مصطفی رستگار</author><pubDate>Sun, 09 Aug 2026 14:26:11 +0330</pubDate></item><item><title>نوشتن crud ساده با nextjs (prisma, zod, route handlers)</title><link>https://virgool.ir/@m.rastegar991/نوشتن-crud-ساده-با-nextjs-prisma-zod-route-handlers-mmxgq13tuxiw</link><description>یک مثال کامل و روان از CRUD (Create, Read, Update, Delete) برای موجودیت Category در Next.js App Router با Route Handlers آماده کردم. فرض می‌کنیم از Prisma به عنوان ORM استفاده می‌کنیم و اعتبارسنجی با Zod انجام می‌شود.ساختار فایل‌هاtextapp/&#xA;├── api/&#xA;│   └── categories/&#xA;│       ├── route.ts          // GET (لیست همه) + POST (ساخت جدید)&#xA;│       └── [id]/&#xA;│           └── route.ts      // GET (یک مورد) + PUT (بروزرسانی) + DELETE (حذف)&#xA;├── lib/&#xA;│   ├── prisma.ts             // کلاینت Prisma&#xA;│   └── validations/&#xA;│       └── category.ts       // اسکیماهای Zod۱. اسکیماهای اعتبارسنجی (Zod)lib/validations/category.tstypescriptimport { z } from &#34;zod&#34;;&#xA;&#xA;// اسکیما برای ساخت و بروزرسانی&#xA;export const categorySchema = z.object({&#xA;  name: z.string().min(2, &#34;نام دسته‌بندی حداقل ۲ کاراکتر باشد&#34;),&#xA;  description: z.string().optional(),&#xA;  slug: z.string().min(2, &#34;اسلاگ حداقل ۲ کاراکتر باشد&#34;),&#xA;  parentId: z.string().uuid(&#34;آیدی والد نامعتبر است&#34;).optional().nullable(),&#xA;  isActive: z.boolean().default(true),&#xA;  order: z.number().int().min(0).default(0),&#xA;});&#xA;&#xA;// تایپ استخراج شده از اسکیما (برای TypeScript)&#xA;export type CategoryInput = z.infer&lt;typeof categorySchema&gt;;&#xA;&#xA;// اسکیما برای آپدیت (همه فیلدها آپشنال)&#xA;export const categoryUpdateSchema = categorySchema.partial();۲. Route Handler اصلی (همه دسته‌بندی‌ها)app/api/categories/route.tstypescriptimport { NextRequest, NextResponse } from &#34;next/server&#34;;&#xA;import { prisma } from &#34;@/lib/prisma&#34;;&#xA;import { categorySchema, type CategoryInput } from &#34;@/lib/validations/category&#34;;&#xA;import { z } from &#34;zod&#34;;&#xA;&#xA;// ============================================&#xA;// GET: دریافت لیست همه دسته‌بندی‌ها (با فیلتر و مرتب‌سازی)&#xA;// ============================================&#xA;export async function GET(request: NextRequest) {&#xA;  try {&#xA;    // دریافت پارامترهای کوئری (مثلاً برای فیلتر یا صفحه‌بندی)&#xA;    const searchParams = request.nextUrl.searchParams;&#xA;    const page = parseInt(searchParams.get(&#34;page&#34;) || &#34;1&#34;);&#xA;    const limit = parseInt(searchParams.get(&#34;limit&#34;) || &#34;10&#34;);&#xA;    const isActive = searchParams.get(&#34;isActive&#34;);&#xA;    const search = searchParams.get(&#34;search&#34;);&#xA;&#xA;    // ساخت شرط‌های فیلتر&#xA;    const where: any = {};&#xA;&#xA;    if (isActive !== null) {&#xA;      where.isActive = isActive === &#34;true&#34;;&#xA;    }&#xA;&#xA;    if (search) {&#xA;      where.OR = [&#xA;        { name: { contains: search, mode: &#34;insensitive&#34; } },&#xA;        { slug: { contains: search, mode: &#34;insensitive&#34; } },&#xA;      ];&#xA;    }&#xA;&#xA;    // دریافت داده‌ها از دیتابیس&#xA;    const [categories, totalCount] = await Promise.all([&#xA;      prisma.category.findMany({&#xA;        where,&#xA;        skip: (page - 1) * limit,&#xA;        take: limit,&#xA;        orderBy: { order: &#34;asc&#34; },&#xA;        include: {&#xA;          parent: {&#xA;            select: {&#xA;              id: true,&#xA;              name: true,&#xA;              slug: true,&#xA;            },&#xA;          },&#xA;          children: {&#xA;            select: {&#xA;              id: true,&#xA;              name: true,&#xA;              slug: true,&#xA;            },&#xA;            take: 5, // فقط ۵ فرزند اول برای جلوگیری از سنگین شدن&#xA;          },&#xA;          _count: {&#xA;            select: { children: true, products: true },&#xA;          },&#xA;        },&#xA;      }),&#xA;      prisma.category.count({ where }),&#xA;    ]);&#xA;&#xA;    return NextResponse.json(&#xA;      {&#xA;        data: categories,&#xA;        pagination: {&#xA;          page,&#xA;          limit,&#xA;          totalCount,&#xA;          totalPages: Math.ceil(totalCount / limit),&#xA;        },&#xA;      },&#xA;      { status: 200 }&#xA;    );&#xA;  } catch (error) {&#xA;    console.error(&#34;Error in GET /api/categories:&#34;, error);&#xA;    return NextResponse.json(&#xA;      { message: &#34;خطا در دریافت دسته‌بندی‌ها&#34; },&#xA;      { status: 500 }&#xA;    );&#xA;  }&#xA;}&#xA;&#xA;// ============================================&#xA;// POST: ایجاد دسته‌بندی جدید&#xA;// ============================================&#xA;export async function POST(request: NextRequest) {&#xA;  try {&#xA;    // ۱. دریافت و پارس کردن بدنه&#xA;    const body = await request.json();&#xA;&#xA;    // ۲. اعتبارسنجی با Zod&#xA;    const result = categorySchema.safeParse(body);&#xA;&#xA;    if (!result.success) {&#xA;      return NextResponse.json(&#xA;        {&#xA;          message: &#34;داده‌های ارسالی نامعتبر هستند&#34;,&#xA;          errors: result.error.issues.map((issue) =&gt; ({&#xA;            field: issue.path.join(&#34;.&#34;),&#xA;            message: issue.message,&#xA;          })),&#xA;        },&#xA;        { status: 400 }&#xA;      );&#xA;    }&#xA;&#xA;    const data: CategoryInput = result.data;&#xA;&#xA;    // ۳. بررسی یکتا بودن اسلاگ&#xA;    const existingCategory = await prisma.category.findUnique({&#xA;      where: { slug: data.slug },&#xA;    });&#xA;&#xA;    if (existingCategory) {&#xA;      return NextResponse.json(&#xA;        { message: &#34;این اسلاگ قبلاً استفاده شده است&#34; },&#xA;        { status: 409 } // Conflict&#xA;      );&#xA;    }&#xA;&#xA;    // ۴. بررسی وجود والد (اگر parentId ارسال شده)&#xA;    if (data.parentId) {&#xA;      const parent = await prisma.category.findUnique({&#xA;        where: { id: data.parentId },&#xA;      });&#xA;&#xA;      if (!parent) {&#xA;        return NextResponse.json(&#xA;          { message: &#34;دسته‌بندی والد وجود ندارد&#34; },&#xA;          { status: 404 }&#xA;        );&#xA;      }&#xA;    }&#xA;&#xA;    // ۵. ایجاد در دیتابیس&#xA;    const newCategory = await prisma.category.create({&#xA;      data: {&#xA;        name: data.name,&#xA;        description: data.description || null,&#xA;        slug: data.slug,&#xA;        parentId: data.parentId || null,&#xA;        isActive: data.isActive ?? true,&#xA;        order: data.order ?? 0,&#xA;      },&#xA;      include: {&#xA;        parent: true,&#xA;      },&#xA;    });&#xA;&#xA;    // ۶. پاسخ موفق&#xA;    return NextResponse.json(&#xA;      {&#xA;        message: &#34;دسته‌بندی با موفقیت ایجاد شد&#34;,&#xA;        data: newCategory,&#xA;      },&#xA;      { status: 201 }&#xA;    );&#xA;  } catch (error) {&#xA;    console.error(&#34;Error in POST /api/categories:&#34;, error);&#xA;    return NextResponse.json(&#xA;      { message: &#34;خطای داخلی سرور&#34; },&#xA;      { status: 500 }&#xA;    );&#xA;  }&#xA;}۳. Route Handler برای یک دسته‌بندی خاص (با آیدی)app/api/categories/[id]/route.tstypescriptimport { NextRequest, NextResponse } from &#34;next/server&#34;;&#xA;import { prisma } from &#34;@/lib/prisma&#34;;&#xA;import { categoryUpdateSchema } from &#34;@/lib/validations/category&#34;;&#xA;&#xA;// ============================================&#xA;// GET: دریافت یک دسته‌بندی با آیدی&#xA;// ============================================&#xA;export async function GET(&#xA;  request: NextRequest,&#xA;  { params }: { params: { id: string } }&#xA;) {&#xA;  try {&#xA;    const { id } = params;&#xA;&#xA;    // بررسی وجود دسته‌بندی&#xA;    const category = await prisma.category.findUnique({&#xA;      where: { id },&#xA;      include: {&#xA;        parent: {&#xA;          select: {&#xA;            id: true,&#xA;            name: true,&#xA;            slug: true,&#xA;          },&#xA;        },&#xA;        children: {&#xA;          select: {&#xA;            id: true,&#xA;            name: true,&#xA;            slug: true,&#xA;            isActive: true,&#xA;          },&#xA;          orderBy: { order: &#34;asc&#34; },&#xA;        },&#xA;        _count: {&#xA;          select: { products: true },&#xA;        },&#xA;      },&#xA;    });&#xA;&#xA;    if (!category) {&#xA;      return NextResponse.json(&#xA;        { message: &#34;دسته‌بندی مورد نظر یافت نشد&#34; },&#xA;        { status: 404 }&#xA;      );&#xA;    }&#xA;&#xA;    return NextResponse.json({ data: category }, { status: 200 });&#xA;  } catch (error) {&#xA;    console.error(&#34;Error in GET /api/categories/[id]:&#34;, error);&#xA;    return NextResponse.json(&#xA;      { message: &#34;خطا در دریافت دسته‌بندی&#34; },&#xA;      { status: 500 }&#xA;    );&#xA;  }&#xA;}&#xA;&#xA;// ============================================&#xA;// PUT: بروزرسانی کامل یا جزئی دسته‌بندی&#xA;// ============================================&#xA;export async function PUT(&#xA;  request: NextRequest,&#xA;  { params }: { params: { id: string } }&#xA;) {&#xA;  try {&#xA;    const { id } = params;&#xA;&#xA;    // ۱. بررسی وجود دسته‌بندی&#xA;    const existingCategory = await prisma.category.findUnique({&#xA;      where: { id },&#xA;    });&#xA;&#xA;    if (!existingCategory) {&#xA;      return NextResponse.json(&#xA;        { message: &#34;دسته‌بندی مورد نظر یافت نشد&#34; },&#xA;        { status: 404 }&#xA;      );&#xA;    }&#xA;&#xA;    // ۲. دریافت و اعتبارسنجی داده‌ها&#xA;    const body = await request.json();&#xA;    const result = categoryUpdateSchema.safeParse(body);&#xA;&#xA;    if (!result.success) {&#xA;      return NextResponse.json(&#xA;        {&#xA;          message: &#34;داده‌های ارسالی نامعتبر هستند&#34;,&#xA;          errors: result.error.issues.map((issue) =&gt; ({&#xA;            field: issue.path.join(&#34;.&#34;),&#xA;            message: issue.message,&#xA;          })),&#xA;        },&#xA;        { status: 400 }&#xA;      );&#xA;    }&#xA;&#xA;    const data = result.data;&#xA;&#xA;    // ۳. بررسی یکتا بودن اسلاگ (اگر تغییر کرده)&#xA;    if (data.slug &amp;&amp; data.slug !== existingCategory.slug) {&#xA;      const slugExists = await prisma.category.findUnique({&#xA;        where: { slug: data.slug },&#xA;      });&#xA;&#xA;      if (slugExists) {&#xA;        return NextResponse.json(&#xA;          { message: &#34;این اسلاگ قبلاً استفاده شده است&#34; },&#xA;          { status: 409 }&#xA;        );&#xA;      }&#xA;    }&#xA;&#xA;    // ۴. جلوگیری از اینکه خودش والد خودش شود!&#xA;    if (data.parentId &amp;&amp; data.parentId === id) {&#xA;      return NextResponse.json(&#xA;        { message: &#34;یک دسته‌بندی نمی‌تواند والد خودش باشد&#34; },&#xA;        { status: 400 }&#xA;      );&#xA;    }&#xA;&#xA;    // ۵. بررسی وجود والد جدید (اگر تغییر کرده)&#xA;    if (data.parentId &amp;&amp; data.parentId !== existingCategory.parentId) {&#xA;      const parent = await prisma.category.findUnique({&#xA;        where: { id: data.parentId },&#xA;      });&#xA;&#xA;      if (!parent) {&#xA;        return NextResponse.json(&#xA;          { message: &#34;دسته‌بندی والد وجود ندارد&#34; },&#xA;          { status: 404 }&#xA;        );&#xA;      }&#xA;    }&#xA;&#xA;    // ۶. بروزرسانی در دیتابیس&#xA;    const updatedCategory = await prisma.category.update({&#xA;      where: { id },&#xA;      data: {&#xA;        name: data.name,&#xA;        description: data.description,&#xA;        slug: data.slug,&#xA;        parentId: data.parentId,&#xA;        isActive: data.isActive,&#xA;        order: data.order,&#xA;      },&#xA;      include: {&#xA;        parent: true,&#xA;        children: {&#xA;          select: {&#xA;            id: true,&#xA;            name: true,&#xA;            slug: true,&#xA;          },&#xA;        },&#xA;      },&#xA;    });&#xA;&#xA;    return NextResponse.json(&#xA;      {&#xA;        message: &#34;دسته‌بندی با موفقیت بروزرسانی شد&#34;,&#xA;        data: updatedCategory,&#xA;      },&#xA;      { status: 200 }&#xA;    );&#xA;  } catch (error) {&#xA;    console.error(&#34;Error in PUT /api/categories/[id]:&#34;, error);&#xA;    return NextResponse.json(&#xA;      { message: &#34;خطای داخلی سرور&#34; },&#xA;      { status: 500 }&#xA;    );&#xA;  }&#xA;}&#xA;&#xA;// ============================================&#xA;// DELETE: حذف دسته‌بندی (با بررسی وابستگی‌ها)&#xA;// ============================================&#xA;export async function DELETE(&#xA;  request: NextRequest,&#xA;  { params }: { params: { id: string } }&#xA;) {&#xA;  try {&#xA;    const { id } = params;&#xA;&#xA;    // ۱. بررسی وجود دسته‌بندی&#xA;    const category = await prisma.category.findUnique({&#xA;      where: { id },&#xA;      include: {&#xA;        _count: {&#xA;          select: {&#xA;            children: true,&#xA;            products: true,&#xA;          },&#xA;        },&#xA;      },&#xA;    });&#xA;&#xA;    if (!category) {&#xA;      return NextResponse.json(&#xA;        { message: &#34;دسته‌بندی مورد نظر یافت نشد&#34; },&#xA;        { status: 404 }&#xA;      );&#xA;    }&#xA;&#xA;    // ۲. بررسی وابستگی‌ها (اگر فرزند یا محصول دارد)&#xA;    if (category._count.children &gt; 0) {&#xA;      return NextResponse.json(&#xA;        {&#xA;          message: &#34;این دسته‌بندی دارای زیردسته است. ابتدا زیردسته‌ها را حذف کنید&#34;,&#xA;          childrenCount: category._count.children,&#xA;        },&#xA;        { status: 409 } // Conflict&#xA;      );&#xA;    }&#xA;&#xA;    if (category._count.products &gt; 0) {&#xA;      return NextResponse.json(&#xA;        {&#xA;          message: &#34;این دسته‌بندی دارای محصول است. ابتدا محصولات را جابجا یا حذف کنید&#34;,&#xA;          productsCount: category._count.products,&#xA;        },&#xA;        { status: 409 }&#xA;      );&#xA;    }&#xA;&#xA;    // ۳. حذف از دیتابیس&#xA;    await prisma.category.delete({&#xA;      where: { id },&#xA;    });&#xA;&#xA;    return NextResponse.json(&#xA;      {&#xA;        message: &#34;دسته‌بندی با موفقیت حذف شد&#34;,&#xA;        deletedId: id,&#xA;      },&#xA;      { status: 200 }&#xA;    );&#xA;  } catch (error) {&#xA;    console.error(&#34;Error in DELETE /api/categories/[id]:&#34;, error);&#xA;    return NextResponse.json(&#xA;      { message: &#34;خطای داخلی سرور&#34; },&#xA;      { status: 500 }&#xA;    );&#xA;  }&#xA;}۴. مثال Prisma Schema (برای مرجع)prisma/schema.prismaprismamodel Category {&#xA;  id          String   @id @default(cuid())&#xA;  name        String&#xA;  description String?&#xA;  slug        String   @unique&#xA;  isActive    Boolean  @default(true)&#xA;  order       Int      @default(0)&#xA;  parentId    String?&#xA;  createdAt   DateTime @default(now())&#xA;  updatedAt   DateTime @updatedAt&#xA;&#xA;  parent      Category? @relation(&#34;CategoryParent&#34;, fields: [parentId], references: [id])&#xA;  children    Category[] @relation(&#34;CategoryParent&#34;)&#xA;  products    Product[]&#xA;&#xA;  @@index([parentId])&#xA;  @@index([slug])&#xA;  @@index([isActive])&#xA;}نکات مهم برای مصاحبه (برای درخشیدن بیشتر)مدیریت خطاهای معنادار: به جای try-catch کلی، خطاهای خاص مثل 409 Conflict برای تکراری بودن یا وابستگی‌ها برگردانده می‌شود.اعتبارسنجی دو مرحله‌ای: اول Zod برای ساختار داده، سپس چک‌های Business Logic مثل یکتا بودن اسلاگ یا وجود والد.بهینه‌سازی کوئری‌ها: استفاده از include برای روابط و select برای فیلدهای خاص (کم کردن حجم پاسخ).صفحه‌بندی و فیلتر: در GET همه دسته‌بندی‌ها، قابلیت جستجو، فیلتر و صفحه‌بندی پیاده‌سازی شده.جلوگیری از خطاهای منطقی: مثل جلوگیری از اینکه یک دسته‌بندی، والد خودش شود.حذف ایمن: قبل از حذف، وابستگی‌ها (فرزندان و محصولات) چک می‌شوند تا از خطای دیتابیس جلوگیری شود.استفاده از Promise.all: برای کوئری‌های موازی (مثل دریافت لیست و تعداد کل) برای بهبود performance.خلاصه برای گفتن در مصاحبه&#34;برای پیاده‌سازی CRUD دسته‌بندی، از Route Handlerهای Next.js استفاده کردم. در مسیر api/categories، متد GET لیست با صفحه‌بندی و فیلتر و متد POST برای ایجاد جدید. در مسیر api/categories/[id]، متد GET برای دریافت یک مورد، PUT برای بروزرسانی (با اعتبارسنجی جزئی با partial()) و DELETE با چک وابستگی‌ها. همه ورودی‌ها با Zod اعتبارسنجی می‌شوند و خطاهای واضح به کلاینت برمی‌گردد. همچنین از Prisma برای ارتباط با دیتابیس و مدیریت روابط (والد-فرزند) استفاده کردم.&#34;</description><category>مصطفی رستگار</category><author>مصطفی رستگار</author><pubDate>Sat, 08 Aug 2026 11:49:05 +0330</pubDate></item><item><title>18- API Routes و Route Handlers (اعتبارسنجی با Zod)</title><link>https://virgool.ir/@m.rastegar991/18-api-routes-و-route-handlers-اعتبارسنجی-با-zod-xl5bsmshs9g0</link><description>۱. معرفی کلی (چرا از API Routes استفاده می‌کنیم؟)«در پروژه‌های Next.js، وقتی نیاز داریم یک Backend کوچک و یکپارچه داشته باشیم، به جای اینکه یک سرور جداگانه با Express یا Flask راه بیندازیم، از قابلیت خود Next.js به نام API Routes استفاده می‌کنیم. این یعنی توی همین پروژه فرانت‌اند، توی پوشه‌ی api، فایل‌هایی می‌نویسیم که نقش سرور رو برای ما بازی می‌کنند. در نسخه‌های جدید Next.js (مخصوصاً اپ روتینگ)، به جای فایل‌های pages/api، از Route Handlers استفاده می‌کنیم که داخل پوشه app قرار می‌گیرند.»۲. تفاوت API Routes قدیم و Route Handlers جدید (اگر سوال کنند)«در نسخه‌های قدیمی‌تر، API‌ها را در پوشه‌ی pages/api می‌نوشتیم و هر فایل، یک تابع handler داشت که با req و res کار می‌کرد. اما در App Router جدید، روش عوض شده. حالا به جای pages/api، داخل پوشه app یک پوشه به نام api می‌سازیم و داخل آن، برای هر مسیر، یک فایل route.js یا route.ts داریم که متدهای مختلف مثل GET، POST، PUT و DELETE را به صورت توابع جداگانه صادر می‌کند. مثلاً توی فایل app/api/users/route.ts، تابع export async function POST(request) { ... } رو می‌نویسیم.»۳. ساختار یک Route Handler ساده (با مثال)«یک مثال خیلی ساده: فرض کنید می‌خواهیم یک API برای ثبت‌نام کاربر بسازیم. توی مسیر app/api/register/route.ts یک تابع POST می‌نویسیم. داخل این تابع، اول با await request.json()، بدنه‌ی درخواست رو می‌گیریم. بعد داده‌ها رو اعتبارسنجی می‌کنیم، و در نهایت یا کاربر رو ذخیره می‌کنیم یا خطا برمی‌گردونیم. در نهایت با NextResponse.json() یک پاسخ JSON برمی‌گردانیم.»۴. اعتبارسنجی با Zod (قسمت اصلی و جذاب)«برای اعتبارسنجی داده‌ها، من از کتابخانه‌ی Zod استفاده می‌کنم. دلیلش اینه که Zod به ما اجازه می‌ده با یک سینتکس خیلی تمیز و شبیه به TypeScript، اسکیما یا قالب داده‌ها رو تعریف کنیم. مثلاً برای ثبت‌نام، یک اسکیما می‌سازم به این شکل:»typescriptconst registerSchema = z.object({&#xA;  name: z.string().min(3, &#34;نام حداقل ۳ کاراکتر باشد&#34;),&#xA;  email: z.string().email(&#34;ایمیل معتبر نیست&#34;),&#xA;  password: z.string().min(6, &#34;رمز حداقل ۶ کاراکتر باشد&#34;)&#xA;});«حالا تابع POST من، داده‌های دریافتی رو با متد safeParse بررسی می‌کنه. safeParse به جای اینکه خطا پرتاب کنه، یک آبجکت برمی‌گردونه که شامل success و data یا error است. اگر success برابر false بود، یعنی داده‌ها نامعتبر هستند و من خطاهای مربوطه رو استخراج کرده و با وضعیت ۴۰۰ برمی‌گردونم. اما اگر اعتبارسنجی موفق بود، ادامه کار رو انجام می‌دم.»۵. کد نهایی (اگر بخواهند دقیق بگویید)«اگر بخوام خلاصه بگم، کد نهایی من به این شکله:»typescriptimport { NextResponse } from &#34;next/server&#34;;&#xA;import { z } from &#34;zod&#34;;&#xA;&#xA;const registerSchema = z.object({&#xA;  name: z.string().min(3),&#xA;  email: z.string().email(),&#xA;  password: z.string().min(6),&#xA;});&#xA;&#xA;export async function POST(request: Request) {&#xA;  try {&#xA;    const body = await request.json();&#xA;    const result = registerSchema.safeParse(body);&#xA;&#xA;    if (!result.success) {&#xA;      return NextResponse.json(&#xA;        { errors: result.error.issues.map((issue) =&gt; issue.message) },&#xA;        { status: 400 }&#xA;      );&#xA;    }&#xA;&#xA;    // اینجا منطق ذخیره‌سازی در دیتابیس&#xA;    // const newUser = await saveUser(result.data);&#xA;&#xA;    return NextResponse.json(&#xA;      { message: &#34;کاربر با موفقیت ثبت شد&#34;, user: result.data },&#xA;      { status: 201 }&#xA;    );&#xA;  } catch (error) {&#xA;    return NextResponse.json(&#xA;      { message: &#34;خطای داخلی سرور&#34; },&#xA;      { status: 500 }&#xA;    );&#xA;  }&#xA;}۶. مزایایی که می‌توانید اضافه کنید (نشان دادن تسلط)«از مزایای این کار، جداسازی منطق اعتبارسنجی از منطق اصلی بیزینس است. همچنین اگر توی جای دیگری از پروژه نیاز به اعتبارسنجی مشابه داشته باشم (مثلاً توی فرم‌های کلاینت‌ساید)، می‌توانم همان اسکیما رو دوباره استفاده کنم. با این کار، هم Frontend و هم Backend من از یک قانون واحد پیروی می‌کنند و کد تمیزتر و قابل نگهداری‌تری داریم.»نکات مهم برای مصاحبه (جوری که بدرخشید)تاکید بر safeParse به جای parse: بگویید از safeParse استفاده می‌کنم چون کنترل خطا را خودم به دست می‌گیرم و سرور کرش نمی‌کند.ارسال خطاهای معنادار: به جای ارسال یک پیام کلی، خطاهای دقیق (مثلاً «ایمیل نامعتبر است») را از result.error.issues استخراج می‌کنم.یکپارچگی با TypeScript: اگر اسکیما را با z.infer استفاده کنید، تایپ خروجی هم به صورت خودکار استخراج می‌شود که نشان‌دهنده‌ی استفاده حرفه‌ای است.تفاوت NextResponse و Response معمولی: بگویید از NextResponse استفاده می‌کنم چون امکانات بیشتری مثل تنظیم cookies و headers را دارد.</description><category>مصطفی رستگار</category><author>مصطفی رستگار</author><pubDate>Sat, 08 Aug 2026 11:44:33 +0330</pubDate></item><item><title>راهنمای جامع مدیریت Token و Cache در Next.js 16</title><link>https://virgool.ir/@m.rastegar991/راهنمای-جامع-مدیریت-token-و-cache-در-nextjs-16-phy0rjzx4m2j</link><description>فهرست مطالبمقدمهمعماری کلیمدیریت Token با Middlewareمدیریت Cache با Next.js Cacheاجرای Refresh در Frontendنکات امنیتی و بهینهسازیجمعبندیمقدمهدر اپلیکیشنهای مدرن Next.js، مدیریت توکنهای احراز هویت و کش دادهها از چالشهای اساسی هستند. در این مقاله، یک راهحل یکپارچه برای مدیریت خودکار refresh token و کش دادهها در Next.js 16 ارائه میدهیم.پیشنیازهاآشنایی با Next.js App Routerدانش پایه از React Server Componentsآشنایی با JWT و مفاهیم احراز هویتمعماری کلیمعماری پیشنهادی شامل سه لایه اصلی است:text┌─────────────────────────────────────────────┐&#xA;│           Client Components                 │&#xA;│  (UI, Interactions, Refresh Button)         │&#xA;└──────────────────┬──────────────────────────┘&#xA;                   │&#xA;┌──────────────────▼──────────────────────────┐&#xA;│           Server Components                 │&#xA;│  (Data Fetching, Caching)                  │&#xA;└──────────────────┬──────────────────────────┘&#xA;                   │&#xA;┌──────────────────▼──────────────────────────┐&#xA;│           Middleware Layer                  │&#xA;│  (Token Management, Authentication)        │&#xA;└──────────────────┬──────────────────────────┘&#xA;                   │&#xA;┌──────────────────▼──────────────────────────┐&#xA;│           External API                      │&#xA;│  (Backend Services)                        │&#xA;└─────────────────────────────────────────────┘مدیریت Token با Middleware1. ساختار Middlewaretypescript// middleware.ts&#xA;import { NextResponse } from &#39;next/server&#39;;&#xA;import type { NextRequest } from &#39;next/server&#39;;&#xA;&#xA;export async function middleware(request: NextRequest) {&#xA;  // 1. دریافت توکنها از کوکی&#xA;  const accessToken = request.cookies.get(&#39;access_token&#39;)?.value;&#xA;  const refreshToken = request.cookies.get(&#39;refresh_token&#39;)?.value;&#xA;&#xA;  // 2. بررسی و تمدید توکن&#xA;  if (accessToken &amp;&amp; await needsRefresh(accessToken)) {&#xA;    const newTokens = await refreshTokens(refreshToken);&#xA;    if (newTokens) {&#xA;      // 3. بهروزرسانی کوکیها و هدرها&#xA;      return updateRequestWithNewTokens(request, newTokens);&#xA;    }&#xA;  }&#xA;&#xA;  // 4. ادامه درخواست با توکن موجود&#xA;  return addTokenToRequest(request, accessToken);&#xA;}&#xA;&#xA;export const config = {&#xA;  matcher: [&#39;/api/:path*&#39;, &#39;/dashboard/:path*&#39;],&#xA;};2. بررسی اعتبار توکنtypescriptasync function needsRefresh(token: string): Promise&lt;boolean&gt; {&#xA;  try {&#xA;    // دیکود کردن JWT برای بررسی زمان انقضا&#xA;    const payload = JSON.parse(atob(token.split(&#39;.&#39;)[1]));&#xA;    const expiryTime = payload.exp * 1000;&#xA;    const timeToExpiry = expiryTime - Date.now();&#xA;    &#xA;    // اگر کمتر از 5 دقیقه به انقضا باقی مانده، تمدید کن&#xA;    return timeToExpiry &lt; 5 * 60 * 1000;&#xA;  } catch {&#xA;    return true; // در صورت خطا، توکن را نامعتبر در نظر بگیر&#xA;  }&#xA;}3. تمدید توکنtypescriptclass TokenManager {&#xA;  private static refreshPromises = new Map&lt;string, Promise&lt;any&gt;&gt;();&#xA;&#xA;  static async refreshToken(refreshToken: string): Promise&lt;any&gt; {&#xA;    // جلوگیری از درخواستهای همزمان&#xA;    if (this.refreshPromises.has(refreshToken)) {&#xA;      return this.refreshPromises.get(refreshToken);&#xA;    }&#xA;&#xA;    const promise = this.doRefresh(refreshToken);&#xA;    this.refreshPromises.set(refreshToken, promise);&#xA;&#xA;    try {&#xA;      return await promise;&#xA;    } finally {&#xA;      this.refreshPromises.delete(refreshToken);&#xA;    }&#xA;  }&#xA;&#xA;  private static async doRefresh(refreshToken: string) {&#xA;    const response = await fetch(`${process.env.API_BASE_URL}/auth/refresh`, {&#xA;      method: &#39;POST&#39;,&#xA;      headers: { &#39;Content-Type&#39;: &#39;application/json&#39; },&#xA;      body: JSON.stringify({ refresh_token: refreshToken }),&#xA;    });&#xA;&#xA;    if (!response.ok) {&#xA;      throw new Error(&#39;Failed to refresh token&#39;);&#xA;    }&#xA;&#xA;    return response.json();&#xA;  }&#xA;}4. به روزرسانی درخواست با توکن جدیدtypescriptfunction updateRequestWithNewTokens(&#xA;  request: NextRequest,&#xA;  newTokens: { access_token: string; refresh_token: string }&#xA;) {&#xA;  const response = NextResponse.next();&#xA;  &#xA;  // تنظیم کوکیهای جدید&#xA;  response.cookies.set(&#39;access_token&#39;, newTokens.access_token, {&#xA;    httpOnly: true,&#xA;    secure: process.env.NODE_ENV === &#39;production&#39;,&#xA;    sameSite: &#39;lax&#39;,&#xA;    maxAge: 60 * 15, // 15 دقیقه&#xA;    path: &#39;/&#39;,&#xA;  });&#xA;&#xA;  // اضافه کردن توکن به هدرهای درخواست&#xA;  const requestHeaders = new Headers(request.headers);&#xA;  requestHeaders.set(&#39;Authorization&#39;, `Bearer ${newTokens.access_token}`);&#xA;&#xA;  return NextResponse.next({&#xA;    request: { headers: requestHeaders },&#xA;  });&#xA;}مدیریت Cache با Next.js Cache1. کش کردن داده ها با use cachetypescript// app/dashboard/page.tsx&#xA;import { cacheLife, cacheTag } from &#39;next/cache&#39;;&#xA;&#xA;export async function getDashboardData(): Promise&lt;Dashboard&gt; {&#xA;  &#39;use cache&#39;;&#xA;  cacheLife(&#39;minutes&#39;); // کش به مدت 5 دقیقه&#xA;  cacheTag(&#39;dashboard_data&#39;); // تگ برای invalidate کردن&#xA;  &#xA;  const response = await fetch(&#39;API_URL/dashboard&#39;, {&#xA;    headers: {&#xA;      &#39;Authorization&#39;: `Bearer ${await getToken()}`,&#xA;    },&#xA;  });&#xA;&#xA;  if (!response.ok) {&#xA;    return {} as Dashboard;&#xA;  }&#xA;&#xA;  return response.json();&#xA;}2. Invalidate کردن کشtypescript// app/actions/refresh.ts&#xA;&#39;use server&#39;;&#xA;&#xA;import { revalidateTag } from &#39;next/cache&#39;;&#xA;&#xA;export async function refreshDashboard() {&#xA;  &#39;use server&#39;;&#xA;  &#xA;  // پاک کردن کش با تگ مشخص&#xA;  revalidateTag(&#39;dashboard_data&#39;);&#xA;  &#xA;  // در صورت نیاز، revalidate مسیرهای خاص&#xA;  // revalidatePath(&#39;/dashboard&#39;);&#xA;  &#xA;  return { success: true };&#xA;}3. ترکیب با Suspense برای تجربه کاربری بهترtypescript// app/dashboard/page.tsx&#xA;import { Suspense } from &#39;react&#39;;&#xA;import { DashboardContent } from &#39;./DashboardContent&#39;;&#xA;import { DashboardSkeleton } from &#39;./DashboardSkeleton&#39;;&#xA;&#xA;export default async function DashboardPage() {&#xA;  return (&#xA;    &lt;Suspense fallback={&lt;DashboardSkeleton /&gt;}&gt;&#xA;      &lt;DashboardContent /&gt;&#xA;    &lt;/Suspense&gt;&#xA;  );&#xA;}&#xA;&#xA;// app/dashboard/DashboardContent.tsx&#xA;async function DashboardContent() {&#xA;  const data = await getDashboardData();&#xA;  return &lt;DashboardView data={data} /&gt;;&#xA;}اجرای Refresh در Frontend1. دکمه Refresh با استفاده از Server Actionstypescript// app/dashboard/RefreshButton.tsx&#xA;&#39;use client&#39;;&#xA;&#xA;import { useTransition } from &#39;react&#39;;&#xA;import { useRouter } from &#39;next/navigation&#39;;&#xA;import { refreshDashboard } from &#39;../actions/refresh&#39;;&#xA;&#xA;interface RefreshButtonProps {&#xA;  className?: string;&#xA;}&#xA;&#xA;export function RefreshButton({ className = &#39;&#39; }: RefreshButtonProps) {&#xA;  const router = useRouter();&#xA;  const [isPending, startTransition] = useTransition();&#xA;&#xA;  const handleRefresh = () =&gt; {&#xA;    startTransition(async () =&gt; {&#xA;      // 1. پاک کردن کش&#xA;      await refreshDashboard();&#xA;      &#xA;      // 2. ریفرش صفحه برای دریافت دادههای جدید&#xA;      router.refresh();&#xA;    });&#xA;  };&#xA;&#xA;  return (&#xA;    &lt;button&#xA;      ={handleRefresh}&#xA;      disabled={isPending}&#xA;      className={`px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 &#xA;        disabled:opacity-50 disabled:cursor-not-allowed transition-colors ${className}`}&#xA;    &gt;&#xA;      {isPending ? (&#xA;        &lt;&gt;&#xA;          &lt;Spinner className=&#34;inline-block mr-2&#34; /&gt;&#xA;          Refreshing...&#xA;        &lt;/&gt;&#xA;      ) : (&#xA;        &#39;🔄 Refresh&#39;&#xA;      )}&#xA;    &lt;/button&gt;&#xA;  );&#xA;}&#xA;&#xA;// کامپوننت Spinner ساده&#xA;function Spinner({ className = &#39;&#39; }) {&#xA;  return (&#xA;    &lt;svg className={`animate-spin h-4 w-4 ${className}`} viewBox=&#34;0 0 24 24&#34;&gt;&#xA;      &lt;circle className=&#34;opacity-25&#34; cx=&#34;12&#34; cy=&#34;12&#34; r=&#34;10&#34; stroke=&#34;currentColor&#34; strokeWidth=&#34;4&#34; fill=&#34;none&#34; /&gt;&#xA;      &lt;path className=&#34;opacity-75&#34; fill=&#34;currentColor&#34; d=&#34;M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z&#34; /&gt;&#xA;    &lt;/svg&gt;&#xA;  );&#xA;}2. استفاده در کامپوننت والدtypescript// app/dashboard/page.tsx&#xA;import { RefreshButton } from &#39;./RefreshButton&#39;;&#xA;&#xA;export default async function DashboardPage() {&#xA;  const data = await getDashboardData();&#xA;&#xA;  return (&#xA;    &lt;div className=&#34;container mx-auto p-4&#34;&gt;&#xA;      &lt;div className=&#34;flex justify-between items-center mb-6&#34;&gt;&#xA;        &lt;h1 className=&#34;text-2xl font-bold&#34;&gt;Dashboard&lt;/h1&gt;&#xA;        &lt;RefreshButton /&gt;&#xA;      &lt;/div&gt;&#xA;      &#xA;      &lt;Suspense fallback={&lt;DashboardSkeleton /&gt;}&gt;&#xA;        &lt;DashboardView data={data} /&gt;&#xA;      &lt;/Suspense&gt;&#xA;    &lt;/div&gt;&#xA;  );&#xA;}3. دکمه Refresh در کامپوننت های تو در تواگر نیاز دارید دکمه Refresh در کامپوننتهای عمیقتر قرار گیرد، از Context استفاده کنید:typescript// app/dashboard/RefreshContext.tsx&#xA;&#39;use client&#39;;&#xA;&#xA;import { createContext, useContext, useTransition } from &#39;react&#39;;&#xA;import { useRouter } from &#39;next/navigation&#39;;&#xA;import { refreshDashboard } from &#39;../actions/refresh&#39;;&#xA;&#xA;interface RefreshContextType {&#xA;  refresh: () =&gt; void;&#xA;  isRefreshing: boolean;&#xA;}&#xA;&#xA;const RefreshContext = createContext&lt;RefreshContextType | undefined&gt;(undefined);&#xA;&#xA;export function RefreshProvider({ children }: { children: React.ReactNode }) {&#xA;  const router = useRouter();&#xA;  const [isRefreshing, startTransition] = useTransition();&#xA;&#xA;  const refresh = () =&gt; {&#xA;    startTransition(async () =&gt; {&#xA;      await refreshDashboard();&#xA;      router.refresh();&#xA;    });&#xA;  };&#xA;&#xA;  return (&#xA;    &lt;RefreshContext.Provider value={{ refresh, isRefreshing }}&gt;&#xA;      {children}&#xA;    &lt;/RefreshContext.Provider&gt;&#xA;  );&#xA;}&#xA;&#xA;export function useRefresh() {&#xA;  const context = useContext(RefreshContext);&#xA;  if (!context) {&#xA;    throw new Error(&#39;useRefresh must be used within RefreshProvider&#39;);&#xA;  }&#xA;  return context;&#xA;}typescript// app/dashboard/layout.tsx&#xA;import { RefreshProvider } from &#39;./RefreshContext&#39;;&#xA;&#xA;export default function DashboardLayout({&#xA;  children,&#xA;}: {&#xA;  children: React.ReactNode;&#xA;}) {&#xA;  return &lt;RefreshProvider&gt;{children}&lt;/RefreshProvider&gt;;&#xA;}typescript// استفاده در هر کامپوننت عمیق&#xA;&#39;use client&#39;;&#xA;import { useRefresh } from &#39;../RefreshContext&#39;;&#xA;&#xA;export function DeepComponent() {&#xA;  const { refresh, isRefreshing } = useRefresh();&#xA;&#xA;  return (&#xA;    &lt;button ={refresh} disabled={isRefreshing}&gt;&#xA;      Refresh from deep component&#xA;    &lt;/button&gt;&#xA;  );&#xA;}نکات امنیتی و بهینه سازی1. تنظیمات امنیتی کوکی هاtypescriptconst cookieOptions = {&#xA;  httpOnly: true, // جلوگیری از دسترسی JavaScript&#xA;  secure: process.env.NODE_ENV === &#39;production&#39;, // فقط HTTPS&#xA;  sameSite: &#39;lax&#39; as const, // جلوگیری از CSRF&#xA;  path: &#39;/&#39;,&#xA;  maxAge: 60 * 15, // 15 دقیقه برای access token&#xA;};2. Rate Limiting برای درخواست های Refreshtypescript// utils/rateLimit.ts&#xA;class RateLimiter {&#xA;  private requests: Map&lt;string, number[]&gt; = new Map();&#xA;&#xA;  isRateLimited(key: string, limit: number = 5, window: number = 60): boolean {&#xA;    const now = Date.now();&#xA;    const windowStart = now - window * 1000;&#xA;    &#xA;    const timestamps = this.requests.get(key) || [];&#xA;    const recentRequests = timestamps.filter(t =&gt; t &gt; windowStart);&#xA;    &#xA;    if (recentRequests.length &gt;= limit) {&#xA;      return true;&#xA;    }&#xA;    &#xA;    recentRequests.push(now);&#xA;    this.requests.set(key, recentRequests);&#xA;    return false;&#xA;  }&#xA;}&#xA;&#xA;// استفاده در middleware&#xA;const rateLimiter = new RateLimiter();&#xA;&#xA;if (rateLimiter.isRateLimited(request.ip || &#39;unknown&#39;)) {&#xA;  return new Response(&#39;Too many refresh requests&#39;, { status: 429 });&#xA;}3. Logging و Monitoringtypescript// utils/logger.ts&#xA;export class Logger {&#xA;  static info(message: string, data?: any) {&#xA;    console.log(`[INFO] ${new Date().toISOString()} - ${message}`, data || &#39;&#39;);&#xA;  }&#xA;&#xA;  static error(message: string, error?: any) {&#xA;    console.error(`[ERROR] ${new Date().toISOString()} - ${message}`, error || &#39;&#39;);&#xA;  }&#xA;&#xA;  static warn(message: string, data?: any) {&#xA;    console.warn(`[WARN] ${new Date().toISOString()} - ${message}`, data || &#39;&#39;);&#xA;  }&#xA;}&#xA;&#xA;// استفاده در middleware&#xA;Logger.info(&#39;Token refresh successful&#39;, { userId: payload.sub });4. Error Handling جامعtypescript// middleware.ts&#xA;async function handleRequest(request: NextRequest) {&#xA;  try {&#xA;    // پردازش اصلی&#xA;    return await processRequest(request);&#xA;  } catch (error) {&#xA;    if (error instanceof TokenExpiredError) {&#xA;      // هدایت به صفحه لاگین&#xA;      return NextResponse.redirect(new URL(&#39;/login&#39;, request.url));&#xA;    }&#xA;    &#xA;    if (error instanceof NetworkError) {&#xA;      // تلاش مجدد با backoff&#xA;      return await retryWithBackoff(() =&gt; processRequest(request));&#xA;    }&#xA;    &#xA;    // خطاهای غیرمنتظره&#xA;    Logger.error(&#39;Unhandled error in middleware&#39;, error);&#xA;    return NextResponse.next();&#xA;  }&#xA;}جمعبندیدر این مقاله، یک راه حل کامل برای مدیریت خودکار توکنها و کش در Next.js 16 ارائه شد:مزایای این معماری:مدیریت متمرکز Token: تمام درخواستها از middleware عبور کرده و توکن ها به صورت خودکار تمدید میشوندکش هوشمند: استفاده از use cache و revalidateTag برای بهینه سازی عملکردتجربه کاربری روان: استفاده از Suspense و Server Actions برای به روزرسانی بدون ریلودامنیت بالا: ذخیره توکنها در کوکی های httpOnly با تنظیمات امنیتی مناسبقابلیت نگهداری: کد تمیز و ماژولار با جدا بودن لایه هابهترین Practices:✅ توکنها را در کوکیهای httpOnly ذخیره کنید✅ از revalidateTag برای invalidate کردن کش استفاده کنید✅ از Suspense برای مدیریت حالتهای loading استفاده کنید✅ Rate Limiting برای جلوگیری از حملات✅ Logging مناسب برای دیباگ و مانیتورینگ✅ استفاده از TypeScript برای type safety</description><category>مصطفی رستگار</category><author>مصطفی رستگار</author><pubDate>Wed, 05 Aug 2026 14:38:51 +0330</pubDate></item><item><title>16- Dynamic Routes و Catch-all Routes (تأثیر بر getStaticPaths)</title><link>https://virgool.ir/@m.rastegar991/16-dynamic-routes-و-catch-all-routes-تأثیر-بر-getstaticpaths-h0d3ookwhode</link><description>«در App Router جدید Next.js، ساختار فولدرها مستقیماً تبدیل به آدرس‌های URL می‌شوند. برای ساخت صفحات پویا (Dynamic)، از نام فولدر یا فایل با کروشه استفاده می‌کنیم. مثلاً فولدری به نام [id] یعنی هر مقداری که در آدرس بیاید، به عنوان پارامتر id در اختیار صفحه قرار می‌گیرد.حالا تفاوت Dynamic با Catch-all در این است که Dynamic فقط یک بخش از آدرس را می‌گیرد، اما Catch-all با سه نقطه (...) نوشته می‌شود و می‌تواند یک یا چند بخش از آدرس را پوشش دهد. مثلاً فولدر [...slug] یعنی هر تعداد بخش که بعد از آدرس اصلی بیاید، به صورت یک آرایه در پارامتر slug ذخیره می‌شود.تأثیر اصلی این دو بر روی getStaticPaths در زمان Build است. چون اگر می‌خواهیم این صفحات را به صورت استاتیک (Static) از پیش رندر کنیم، باید در تابع generateStaticParams (که معادل جدید getStaticPaths است) دقیقاً مشخص کنیم که چه مقادیری قرار است تولید شوند.برای Dynamic معمولی، لیستی از اشیاء با همان کلید (مثلاً { id: &#39;1&#39; }) برمی‌گردانیم. اما برای Catch-all، مقدار را به صورت یک آرایه در همان کلید (slug) تحویل می‌دهیم. مثلاً { slug: [&#39;products&#39;, &#39;shoes&#39;, &#39;nike&#39;] } یعنی آدرس /products/shoes/nike تولید می‌شود.نکته مهم این است که اگر از generateStaticParams استفاده نکنیم، Next.js این صفحات را در زمان درخواست (SSR یا ISR) رندر می‌کند و دیگر تأثیری روی Build ندارد. همچنین اگر پارامتری در generateStaticParams تعریف نشود، با استفاده از fallback: true یا &#39;blocking&#39; می‌توانیم صفحات جدید را موقع بازدید کاربر بسازیم.به طور خلاصه، App Router مسیرها را ساده‌تر کرده، اما برای رندر استاتیک، ما موظفیم در generateStaticParams تمام حالت‌های ممکن Dynamic و Catch-all را مشخص کنیم تا Next.js بتواند هنگام Build، فایل‌های HTML متناظر با آن‌ها را تولید کند.»نکات کلیدی برای تسلط بیشتر (برای خودت):۱. تفاوت نحوه نوشتن:[id] → فقط یک بخش: page/[id] → آدرس: /page/5[...slug] → یک یا چند بخش: page/[...slug] → آدرس: /page/a/b/c (خواهد شد slug: [&#39;a&#39;,&#39;b&#39;,&#39;c&#39;])۲. نحوه نوشتن در generateStaticParams (معادل getStaticPaths):js// برای Dynamic ساده:&#xA;generateStaticParams() {&#xA;  return [{ id: &#39;1&#39; }, { id: &#39;2&#39; }];&#xA;}&#xA;&#xA;// برای Catch-all:&#xA;generateStaticParams() {&#xA;  return [&#xA;    { slug: [&#39;products&#39;, &#39;shoes&#39;] },&#xA;    { slug: [&#39;blog&#39;, &#39;nextjs&#39;, &#39;tutorial&#39;] }&#xA;  ];&#xA;}۳. نکته فنی:در App Router، دیگر از getStaticProps و getStaticPaths استفاده نمی‌کنیم، بلکه به جای آن‌ها از generateStaticParams و داخل خود کامپوننت از fetch یا کش استفاده می‌کنیم.۴. جمع‌بندی کاربردی:اگر تعداد صفحات محدود و مشخص است، حتماً از generateStaticParams استفاده کن تا سایتت سریع‌تر شود.اگر تعداد صفحات نامحدود یا خیلی زیاد است، از fallback: &#39;blocking&#39; استفاده کن تا فقط صفحات پرطرفدار اول ساخته شوند.</description><category>مصطفی رستگار</category><author>مصطفی رستگار</author><pubDate>Wed, 29 Jul 2026 14:31:40 +0330</pubDate></item><item><title>14- Next.js:`App Router vs Pages Router (تفاوت و استراتژی مهاجرت)</title><link>https://virgool.ir/@m.rastegar991/14-nextjsapp-router-vs-pages-router-تفاوت-و-استراتژی-مهاجرت-k126cfuk0ytm</link><description>۱. تعریف کلی (برای شروع):&#34;برای اینکه صحبتم را شفاف کنم، اجازه دهید از یک مثال ساده استفاده کنم. قبلاً در Next.js (نسخه ۱۳ به پایین)، ما یک سیستم داشتیم به نام Pages Router. این سیستم مثل یک دفترچه تلفن قدیمی بود: هر فایلی که در پوشه pages می‌گذاشتیم، یک آدرس (مسیر) جدید در سایت می‌شد. اما مشکل این بود که تمام صفحات ما ایستا (Static) بودند و برای کارهای پیشرفته‌تر، مجبور بودیم کدهای پیچیده‌ای بنویسیم.&#34;۲. تفاوت اصلی (مفهوم کلیدی):&#34;اما با آمدن App Router در نسخه ۱۳.۴ به بعد، طرز فکر ما عوض شد. تفاوت اصلی در سه بخش خلاصه می‌شود:۱. ساختار پوشه‌ها (Folder vs File): در App Router دیگر نام فایل تعیین‌کننده مسیر نیست، بلکه نام پوشه مهم است. مثلاً یک پوشه به نام dashboard با یک فایل خاص page.js داخلش، یعنی مسیر داشبورد. این اجازه می‌دهد که فایل‌های کمکی را در کنار همان صفحه قرار دهیم (مثل استایل یا کامپوننت‌های اختصاصی).۲. رندرینگ (سرویس‌دهی صفحات): اینجا مهم‌ترین تفاوت است. در Pages Router، فقط دو حالت داشتیم: استاتیک (SSG) یا سروری (SSR) در سطح کل صفحه. اما در App Router، هر بخش از صفحه می‌تواند استراتژی رندرینگ خودش را داشته باشد! یعنی می‌توانیم یک کامپوننت را داخل صفحه به صورت &#39;use client&#39; (کلاینت) و کامپوننت دیگر را به صورت &#39;use server&#39; (سروری) تعریف کنیم. این یعنی کنترل کامل روی عملکرد.۳. بهبود عملکرد (Performance): App Router از قابلیت جدیدی به نام Server Components استفاده می‌کند که باعث می‌شود بخش اعظم کدها در سرور اجرا شوند و حجم جاوااسکریپتی که به مرورگر کاربر می‌فرستیم، به شدت کم شود. نتیجه؟ سایت‌های بسیار سریع‌تر و سئو بهتر.&#34;۳. استراتژی مهاجرت (چطور از قدیم به جدید برویم؟):&#34;اگر پروژه‌ای با Pages Router داریم، نباید یک‌دفعه کل پروژه را عوض کنیم. استراتژی من هوشمندانه است:قدم اول (هم‌زیستی): خوشبختانه Next.js اجازه می‌دهد که هم‌زمان از هر دو مسیریاب استفاده کنیم. یعنی می‌توانم پوشه app را کنار پوشه pages بسازم و به تدریج صفحات جدید را با App Router بنویسم.قدم دوم (اولویت با صفحات جدید): برای صفحات جدید یا صفحاتی که بیشترین بازدید را دارند و نیاز به بهینه‌سازی دارند، اولویت می‌دهم تا با معماری جدید نوشته شوند.قدم سوم (نکات فنی): در حین مهاجرت، باید دو نکته را رعایت کنم:۱. getServerSideProps یا getStaticProps دیگر وجود ندارند و باید با fetch مستقیم در کامپوننت‌های سروری جایگزین شوند.۲. فایل _app.js و _document.js حذف می‌شوند و به جای آن از layout.js ریشه‌ای و loading.js برای نمایش بارگذاری استفاده می‌کنم.&#34;۴. جمع‌بندی نهایی (نتیجه‌گیری):&#34;در یک جمله، App Router فقط یک آپدیت ساده نیست؛ بلکه یک تغییر پارادایم از Page-based به Component-based است. با این کار، توسعه‌دهنده قدرت بیشتری برای بهینه‌سازی دارد و کاربر نهایی سایتی بسیار سریع‌تر و روان‌تر تجربه می‌کند. من برای پروژه‌های جدید حتماً از App Router شروع می‌کنم و برای پروژه‌های قدیمی، مهاجرت تدریجی و هوشمندانه را در پیش می‌گیرم.&#34;نکات طلایی برای مصاحبه (که در متن بالا گنجانده شده):به جای گفتن &#34;فرق در فایل است&#34;، بگویید &#34;از File-based به Folder-based&#34;.حتماً به Server Components اشاره کنید (چون نشان می‌دهد به‌روز هستید).نگویید &#34;همه چیز را عوض می‌کنیم&#34;، بگویید &#34;مهاجرت تدریجی&#34; برای نشان دادن مدیریت ریسک.</description><category>مصطفی رستگار</category><author>مصطفی رستگار</author><pubDate>Wed, 29 Jul 2026 13:46:58 +0330</pubDate></item><item><title>13- Middleware در Next.js (مقایسه با Express.js middleware)</title><link>https://virgool.ir/@m.rastegar991/13-middleware-در-nextjs-مقایسه-با-expressjs-middleware-r8acz1bltczb</link><description>Middleware در Next.js چیست؟Middleware در Next.js یک تابع است که قبل از کامل شدن درخواست (Request) اجرا می‌شود. یعنی بین لحظه‌ای که کاربر درخواست می‌دهد و زمانی که صفحه یا پاسخ نهایی نمایش داده می‌شود، یک لایه میانی قرار دارد که می‌توانیم در آن منطق خود را اجرا کنیم.این Middleware در پوشه‌ی ریشه پروژه، در فایل middleware.ts قرار می‌گیرد و به طور پیش‌فرض روی همه‌ی مسیرها اجرا می‌شود، مگر اینکه با استفاده از matcher بعضی مسیرها را حذف یا محدود کنیم.کاربردهای اصلی Middleware در Next.js:احراز هویت (Authentication): مثلاً اگر کاربر لاگین نکرده باشد، او را به صفحه‌ی ورود هدایت کنیم.مدیریت مسیرها (Routing): تغییر مسیر بر اساس موقعیت جغرافیایی یا نوع دستگاه (موبایل/دسکتاپ).تنظیم هدرها (Headers): افزودن هدرهای امنیتی یا کش به پاسخ.لاگ‌گیری (Logging): ثبت اطلاعات درخواست‌ها برای تحلیل.تفاوت Middleware در Next.js با Express.jsاین سؤال خیلی رایج است. تفاوت‌های اصلی را در چند نقطه خلاصه می‌کنم:یک مثال ساده از Middleware در Next.js:typescript// middleware.ts&#xA;import { NextResponse } from &#39;next/server&#39;;&#xA;import type { NextRequest } from &#39;next/server&#39;;&#xA;&#xA;export function middleware(request: NextRequest) {&#xA;  // اگر کاربر توکن احراز هویت نداشت، به لاگین ببر&#xA;  const token = request.cookies.get(&#39;token&#39;);&#xA;  if (!token &amp;&amp; request.nextUrl.pathname.startsWith(&#39;/dashboard&#39;)) {&#xA;    return NextResponse.redirect(new URL(&#39;/login&#39;, request.url));&#xA;  }&#xA;&#xA;  // ادامه‌ی مسیر&#xA;  return NextResponse.next();&#xA;}&#xA;&#xA;// فقط روی مسیرهای dashboard اجرا کن&#xA;export const config = {&#xA;  matcher: &#39;/dashboard/:path*&#39;,&#xA;};خلاصه‌ی نهایی برای مصاحبه:Middleware در Next.js یک لایه میانی در لبه (Edge) است که قبل از نمایش هر صفحه اجرا می‌شود و برای کنترل دسترسی، تغییر مسیر، و مدیریت درخواست‌ها استفاده می‌شود. برخلاف Express.js که Middleware‌ها روی سرور Node.js اجرا می‌شوند و انعطاف بیشتری در پاسخ‌دهی دارند، Middleware Next.js سریع‌تر و سبک‌تر است و بیشتر روی مسیردهی و امنیت تمرکز دارد.</description><category>مصطفی رستگار</category><author>مصطفی رستگار</author><pubDate>Wed, 29 Jul 2026 13:41:13 +0330</pubDate></item></channel></rss>