وب مدرن، رابط کاربری و تجربهی وب · فصل ۴ — React، State و Visual Regression · 46
Server State و Typed Routing در React
میتواند «Server State و Typed Routing در React» را در یک feature محدود طراحی/پیادهسازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.
خلاصهٔ درس
میتواند «Server State و Typed Routing در React» را در یک feature محدود طراحی/پیادهسازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.
خروجی این درس
میتواند «Server State و Typed Routing در React» را در یک feature محدود طراحی/پیادهسازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.
پیشنیازها
فهرستی که درست دیده میشود، ممکن است قرارداد state اشتباهی داشته باشد
یک صفحه React میتواند از نظر ظاهری کاملاً درست باشد، اما سه نوع state را بیصدا با هم مخلوط کرده باشد. فیلتر دستهبندی ممکن است در useState نگهداری شود در حالی که باید bookmarkable و قابل اشتراک باشد. دادهٔ محصول ممکن است از سرور دریافت و دوباره در state محلی کپی شود، در حالی که مرجع اصلی آن سیستم راهدور است. Query Key ممکن است فقط products باشد، با اینکه نتیجه به category و page وابسته است. Route API هم میتواند در TypeScript تایپ مناسبی داشته باشد، ولی ورودی malformed از URL هنوز در runtime وارد برنامه شود.
سناریوی این درس یک product list قابل جستوجو است. category و page باید با refresh و share باقی بمانند، پس URL/search state هستند. باز یا بسته بودن details dialog یک UI memory موقت است و محلی میماند. ردیفهای محصول server state هستند. Query identity باید همهٔ dependencyهایی را که نتیجه را تغییر میدهند در خود داشته باشد. پس از mutation موفق، query family مرتبط invalidate میشود؛ اما invalidation بهتنهایی به معنای «دادهٔ تازه از سرور رسیده است» نیست. در نهایت، search input خام باید در مرز runtime validate شود و بعد بهعنوان typed route state مورد اعتماد قرار گیرد.
خروجی نهایی Server-State & Typed Route Contract Record v1 است. این رکورد implementation، observation، inference، failure mutation، repair، transfer و residual limitation را از هم جدا میکند. ظاهر درست صفحه، معیار پایان درس نیست.
محدوده، مسیر یادگیری و سطح evidence
این درس ادامهٔ مستقیم WEB-012 است. از آن درس فقط یک اصل پایه وارد میکنیم: component-local state حافظهای است که مالکیتش در درخت React مشخص است. WEB-012 دو دامنهٔ مالکیت دیگر اضافه میکند. URL state برای navigation و وضعیت قابل share/bookmark مناسب است. Server state دادهای است که مالک نهاییاش سیستم راهدور است و client فقط میتواند آن را cache، refetch یا invalidate کند؛ cache شدن، آن را به source of truth تبدیل نمیکند.
این درس ادعا نمیکند TanStack Query یا TanStack Router برای همهٔ برنامههای React الزامیاند. این ابزارها fixture مشخص درس هستند چون قراردادها را قابل مشاهده میکنند. مهارت پایدار این است: state را طبقهبندی کنید، remote identity را کامل بسازید، رفتار freshness/invalidation را صریح کنید، typeها را در route context ثبتشده عبور دهید و ورودی URL را در runtime اعتبارسنجی کنید.
اگر query cache و route tree برایتان جدید است مسیر foundation را دنبال کنید. اگر APIها را میشناسید ولی cache را با مرجع اصلی یا type inference را با runtime validation اشتباه میگیرید، مسیر repair مناسبتر است. اگر باتجربه هستید میتوانید syntax را فشرده کنید، ولی evidence و transfer حذف نمیشوند.
مدل ذهنی: raw URL → runtime validation → typed route state → query identity → remote lifecycle → evidence
مدل اصلی یک pipeline با مرزهای روشن است:
- Raw URL/Search Input ورودی کاربر است و باید untrusted فرض شود.
- Runtime Validation مقدار را parse، normalize، default یا reject میکند.
- Typed Route State خروجی معتبرشدهای است که route APIها در اختیار برنامه میگذارند.
- Query Identity همهٔ dependencyهایی را که نتیجهٔ remote را تغییر میدهند در خود دارد؛ مثل category و page.
- Fetch/Cache Lifecycle تصمیم میگیرد داده از cache خوانده شود، fetch شود، stale شود یا refetch شود.
- Mutation و Invalidation اعلام میکنند knowledge موجود دربارهٔ remote state دیگر قابل اعتماد کامل نیست و باید طبق policy دوباره reconcile شود.
- Rendered Evidence observation واقعی است و باید از inference معماری جدا ثبت شود.
Details dialog در کنار این pipeline است. selectedProductId در این سناریو local state است چون الزام bookmark/share برای dialog نداریم. اگر requirement محصول تغییر کند و انتخاب محصول هم باید linkable شود، طبقهبندی باید دوباره انجام شود. مکانیزم از contract محصول پیروی میکند، نه از یک قاعدهٔ حفظی.
پایه: پنج قراردادی که نباید با هم قاطی شوند
۱. قرارداد State Classification
اول بپرسید چه سیستمی مالک حقیقت است و کاربر چه نوع persistence میخواهد. UI memory موقت معمولاً محلی است. انتخابهای navigation که باید در refresh و share باقی بمانند در URL قرار میگیرند. دادهٔ remote در server-state mechanism مدیریت میشود. این تفکیک جلوی یک anti-pattern رایج را میگیرد: یک client store بزرگ که dialog، filter و کپی دادهٔ سرور را بدون مرز مالکیت کنار هم نگه میدارد.
برای آزمون، یکی از boundaryها را تغییر دهید. صفحه را refresh کنید. URL را در tab جدید باز کنید. داده را refetch کنید. Dialog را ببندید و باز کنید. اگر value در مرز اشتباه باقی میماند یا از بین میرود، classification باید دوباره بررسی شود.
۲. قرارداد Query Identity
Query Key فقط اسم component نیست؛ identity یک remote dependency است. اگر نتیجه با category یا page عوض میشود، این دو باید در identity حضور داشته باشند. استفاده از ['products'] برای تمام صفحات و دستهها یعنی route stateهای متفاوت میتوانند در یک cache slot collide کنند. شکل مناسب میتواند ['products', { category, page }] یا هر نمایش پایدار معادل باشد.
مهم، کاملبودن dependency است نه شکل ظاهری string. Evidence باید دو route state نرمالشده و دو query identity متفاوت را نشان دهد. سپس یک dependency را حذف کنید و شرایط collision را عمداً بسازید تا checker و تحلیل شما به آن واکنش نشان دهد.
۳. قرارداد Freshness و Invalidation
Cache فقط مشاهدهٔ client از remote state است. lifecycle آن باید صریح باشد. راهنمای فعلی TanStack Query، invalidation را بهصورت stale کردن queryهای matching و refetch شدن queryهای active طبق تنظیمات توضیح میدهد. بنابراین ادعا را محدود نگه میداریم: invalidation وضعیت lifecycle cache را تغییر میدهد؛ خودش مدرک رسیدن پاسخ تازه از سرور نیست.
بعد از mutation موفق، query family متأثر را مشخص و invalidate کنید. اگر یک محصول در چند category/page قابل نمایش است، prefix مناسب ممکن است از یک exact page گستردهتر باشد. دامنهٔ invalidation باید از dependency داده استخراج شود، نه از راحتی کدنویسی.
۴. قرارداد Typed Route Context
TypeScript میتواند اطلاعات route را از route definition به Link، navigation و route hookها منتقل کند، به شرط آنکه برنامه از route tree/context ثبتشده استفاده کند. این کار خطاهای refactor و mismatch را کم میکند، اما runtime boundary را حذف نمیکند. Type میگوید برنامهٔ کامپایلشده چه انتظاری دارد؛ URL از بیرون type system وارد میشود.
در lab، navigation و خواندن search params از قرارداد /products انجام میشود و parsing پراکنده در چند component نداریم. این باعث میشود قرارداد navigation یک محل قابل بازرسی داشته باشد.
۵. قرارداد URL Runtime Validation
Search params ورودی قابل کنترل توسط کاربر است. قبل از اعتماد، آن را validate کنید. Fixture مقدار page را به integer مثبت و category را به مجموعهٔ مجاز normalize میکند. نوشتن as ProductSearch روی یک object malformed آن را معتبر نمیکند؛ فقط compiler را ساکت میکند.
Failure probe این تفاوت را واضح میکند: runtime validator را با type assertion جایگزین کنید و URL نامعتبر بدهید. اگر برنامه مقدار نامعتبر را trusted state فرض کند، روشن میشود که compile-time typing جای validation اجرایی را نگرفته است.
توضیح عمیق: چرا shortcutهای منطقی، bugهای پنهان میسازند
خطرناکترین اشتباهها در حالت عادی قابل قبول به نظر میرسند. کپی کردن query.data داخل useState راحت است، چون ویرایش local را ساده میکند؛ اما حالا دو نمایش مستقل داریم: query data و local copy. Refetch ممکن است اولی را بهروز کند، در حالی که UI همچنان دومی را render کند. اگر draft لازم است، همان draft را با scope مشخص local نگه دارید؛ کل remote collection را به source of truth دوم تبدیل نکنید.
Query key ناقص هم در first render معمولاً مشکلی نشان نمیدهد. Bug زمانی ظاهر میشود که route state دیگری نتیجهٔ متفاوتی با همان identity درخواست کند. بدون changed-state probe، تیم ممکن است مدتها false confidence داشته باشد.
Type assertion هم failure خاموش میسازد. raw as ProductSearch هیچ دادهای را parse یا validate نمیکند. Runtime validation یک boundary اجرایی است، نه annotation.
Invalidation نیز محل overclaim است. میتوانید invalidateQueries را await کنید و callbackهای mutation را با Promise هماهنگ کنید، ولی evidence باید API event را از observation محصول جدا نگه دارد. ثبت کنید چه زمانی invalidation درخواست شد، کدام key family هدف بود، چه refetch/loading stateای دیده شد و چه دادهای بعداً render شد. جملهٔ «invalidate یعنی fresh» این تفاوتها را حذف میکند.
مثال کامل: فیلتر bookmarkable، dialog محلی، محصولات remote
با /products?category=books&page=2 شروع کنید. Validator خروجیای مثل { category: 'books', page: 2 } تولید میکند. Query همان dependencyها را در identity استفاده میکند. Fetch function هم همان مقادیر normalizeشده را دریافت میکند. بازکردن details dialog فقط selectedProductId محلی را تغییر میدهد و route را عوض نمیکند، چون share کردن dialog بخشی از requirement این fixture نیست.
حالا category را به electronics تغییر دهید. Route state و query identity هر دو تغییر میکنند، بنابراین cache میتواند این دو remote dependency را از هم جدا کند. در back navigation ممکن است cache طبق freshness policy دوباره استفاده شود، ولی مرجع authoritative همچنان remote system است.
بعد mutation نام محصول را اجرا کنید. در onSuccess خانوادهٔ products را invalidate کنید. در evidence دو چیز جدا ثبت کنید: «invalidation صادر شد» و «بعداً list بهروز مشاهده شد». این جداسازی مهم است چون inactive query، error، config و timing میتوانند رفتار visible update را تغییر دهند.
در پایان refresh یا share کنید. category و page باقی میمانند چون URL state هستند. Dialog بسته میشود چون local state بوده است. این از دست رفتن داده نیست؛ رفتار مورد انتظار contract است.
Failure Probeها: evidence باید به شکستن مدل واکنش نشان دهد
Mutation A — حذف category/page از Query Key
Key را به ['products'] تبدیل کنید. قبل از اجرا prediction بنویسید: دو route state متفاوت اکنون یک cache identity دارند. Checker و changed-state case باید این نقص را آشکار کنند. تشخیص صحیح «query identity ناقص است» خواهد بود، نه «کتابخانه cache خراب است».
Mutation B — جایگزینی validation با cast
Validator را با assertion یا pass-through بدون validation عوض کنید و ?category=unknown&page=-4 بدهید. اگر مقادیر malformed بهعنوان trusted route state وارد شوند، compiler هیچ validation runtime انجام نداده است. Repair یعنی بازگرداندن parse/default/reject اجرایی.
Mutation C — Invalidate کردن prefix اشتباه
بعد mutation، ['product'] را invalidate کنید در حالی که listها زیر ['products', ...] هستند. Mutation remote میتواند موفق باشد ولی cached list visible همچنان stale بماند. Target را اصلاح و همان observation را دوباره اجرا کنید.
Mutation D — کپی server data در local state مستقل
از useState(query.data ?? []) استفاده و rows را render کنید. بعد refetch را اجرا کنید. ممکن است query data تغییر کند ولی local copy بهروز نشود. Repair این است که list اصلی مستقیماً query data را بخواند و فقط draft یا UI memory واقعی local باشد.
پل برای برنامهنویس باتجربه: Ownership و Identity، نه شعار کتابخانه
برای مهندس باتجربه سؤال پایدار این نیست که «آیا Query Library استفاده کنیم؟». سؤال این است: مالک این value کیست، چه dependencyهایی identity داده را میسازند، input بیرونی کجا validate میشود و چه observationی میتواند مدل ما را falsify کند؟ ابزار خوب این قراردادها را قابل بیانتر میکند، اما semantics محصول را تعیین نمیکند.
Query key را projection dependencyهای remote بدانید. URL schema را boundary contract بدانید. Invalidation را lifecycle operation بدانید. Local state را عمداً local نگه دارید. این vocabulary را میتوان به router/cache client دیگر منتقل کرد بدون اینکه APIهای متفاوت را یکی فرض کنیم.
در code review دنبال duplication پنهان بگردید: filterهای URL که دوباره در component state mirror شدهاند، rowهای remote که در store کپی شدهاند، چند parser برای یک search param، یا mutationی که prefix نامرتبط را invalidate میکند. اینها علامت بازگشت به contract قبل از افزودن abstraction جدید هستند.
Alternativeها و Trade-offها
یک برنامهٔ کوچک میتواند بهجای TanStack Router/Query از URLSearchParams، fetch hook سفارشی و request state صریح استفاده کند. اگر feature کوچک و قراردادها روشن باشند، این انتخاب میتواند مناسب باشد. هزینهاش این است که identity، freshness، cancellation، validation و share behavior بیشتر به کد خود برنامه منتقل میشود.
Router دارای validated search params میتواند navigation state را متمرکز و refactor را امنتر کند. Query cache هم identity و lifecycle دادهٔ remote را متمرکز میکند. هیچکدام بهتنهایی correctness محصول را اثبات نمیکنند. abstraction زمانی ارزش دارد که contract را شفافتر و قابل تستتر کند.
برای mutation هم strategy واحد نداریم. اگر response سرور authoritative و شکل cache دقیقاً معلوم است، exact cache update میتواند refetch را کم کند. اگر یک record در viewهای متعدد حضور دارد، invalidation خانوادهٔ مرتبط ممکن است سادهتر و امنتر باشد. درس فقط میخواهد strategy با dependency منطبق باشد و evidence دقیقاً بگوید چه چیزی ثابت شده است.
تمرین: اول Fixture را Repair کنید، بعد Method را Transfer کنید
Guided Repair
پوشهٔ lab/WEB012-LAB1/learner/starter را باز کنید. قبل از تغییر کد، ردیفهای contract را بنویسید: کدام value local، URL یا server state است؟ چه dependencyهایی نتیجهٔ محصول را تغییر میدهند؟ URL malformed باید چه شود؟ Mutation کدام query family را متأثر میکند؟
اول runtime search validation را repair کنید تا evidence بعدی از route state normalizeشده استفاده کند. بعد query key را از category و page بسازید. کپی مستقل query result را حذف کنید. Dialog selection را local نگه دارید. در پایان پس از mutation fake API، query family درست را invalidate کنید.
Public checker ساختار و schema evidence را بررسی میکند؛ مرورگر را بهجای شما مشاهده نمیکند. فقط observationهایی را ثبت کنید که واقعاً اجرا و دیدهاید. Attestation اولیه عمداً false است تا evidence ساختگی از template به learner منتقل نشود.
Independent Transfer
Transfer fixture موضوع را به order list تغییر میدهد: status/page در URL، details dialog محلی و mutation remote. State classification و query identity را دوباره استخراج کنید. Product solution را token-by-token کپی نکنید. روش باید route schema جدید، key dependencyهای جدید، invalidation target درست و local dialog state جدا تولید کند.
Transfer وقتی کامل است که یک residual limitation نوشته شود. مثلاً deterministic fake API رفتار raceهای شبکه، authorization change یا update همزمان چند client را اثبات نمیکند.
Verification: Contract Record چه چیزی باید ثابت کند
رکورد شش بعد اجباری دارد. Implementation artifact محل route/query/mutation/local-state را معرفی میکند. Observable behavior فقط چیزهای واقعاً دیدهشده را ثبت میکند. Failure mutation result prediction و failure مشاهدهشده دارد. Repair result همان probe را بعد repair تکرار میکند. Changed-context transfer نشان میدهد روش با عوض شدن context هم کار میکند. Residual limitation جلوی تبدیل lab محدود به ادعای جهانی را میگیرد.
Observation و inference را جدا بنویسید. «URL بعد navigation برابر ?category=books&page=2 بود» observation است. «پس در این fixture filter state bookmarkable حفظ شد» inference است. «در همهٔ محیطها همین رفتار تضمین است» overclaim خواهد بود.
Hidden oracle قراردادهای ساختاری را بررسی میکند که با comment ساده نباید قابل spoof باشند. PASS شدن آن لازم است، اما evidence مرورگر مشاهدهنشده تولید نمیکند.
ارزیابی و پایان درس
Retrieval: local/URL/server state را طبقهبندی و یک non-goal بنویسید. Application: product list را repair و record را کامل کنید. Diagnosis: یک failure mutation را پیشبینی و اجرا کنید. Decision: یک انتخاب identity/invalidation را در برابر alternative دفاع کنید. Transfer: order-list variant را مستقل حل کنید.
Failureهای blocking شامل ندیدن critical mutation، استفاده از ظاهر بهعنوان تنها مدرک، حذف remote dependency از query identity، اعتماد به type assertion بهعنوان runtime validation، یا ادعای اینکه invalidation خودش ثابت میکند data جدید رسیده است.
پایان درس «صفحه render شد» نیست. پایان درس state contract قابل بازرسی و evidenceای است که هنگام شکستن عمدی contract واکنش نشان میدهد.
منابع و مرز Freshness
این درس freshness-sensitive است. Factهای بیرونی محدود و به مستندات رسمی فعلی TanStack متصلاند. مستندات Query، invalidation را stale کردن queryهای matching و refetch شدن active matchها طبق configuration توضیح میدهد. مستندات Router هم type propagation را از runtime validation ورودی search جدا میکند. این موارد در 2026-09-13 دوباره بررسی شدهاند و باید تا review_due یا هنگام تغییر guidance بازبینی شوند.
روش پایدار فراتر از ابزارهاست: مالکیت state را طبقهبندی کنید، input بیرونی را در runtime validate کنید، dependency identity را کامل بسازید، lifecycle را صریح کنید و evidence را بدون overclaim ثبت کنید.
ارزیابی
پاسخنامهی این درس در بستهی آموزش وجود ندارد؛ خودت جواب بده و صادقانه علامت بزن. «نیاز به مرور» درس را به صف تکرار میفرستد.
WEB-012-S1 retrieval
انتخاب dialog، category/page و product rows را به local، URL یا server state طبقهبندی کنید و یک non-goal بنویسید.
WEB-012-S2 application
product route را طوری repair کنید که search paramها در runtime validate شوند، query identity شامل category/page باشد، remote rows در local state کپی نشوند و mutation خانوادهٔ درست products را invalidate کند.
WEB-012-S3 diagnosis
یکی از mutationهای query key، validation، invalidation یا کپی server state را پیشبینی و اجرا کنید، diagnose و repair کنید.
WEB-012-S4 decision
دامنهٔ query key و invalidation انتخابی را در برابر یک alternative دفاع کنید، بدون ادعای قاعدهٔ جهانی.
WEB-012-S5 transfer
روش را به order-list fixture با status/page در URL، details dialog محلی و mutation remote منتقل کنید.
ملاکهای ارزیابی (rubric)
Critical dimensions:
- Contract Correctness — State classification, query identity, invalidation semantics, typed route context and runtime URL validation preserve the bounded lesson contract.
- Evidence Quality — Observations, mutation, repair and transfer evidence are traceable and do not infer runtime facts from types or appearance alone.
- Scope Discipline — Cache, router and invalidation claims remain bounded; the cache is not described as the server and type inference is not described as runtime validation.
- Transfer Independence — The changed-context order-list task is solved without decisive scaffold or guided-domain leakage.
Blocking assessments: S2, S3, S5. Non-blocking assessments still require contract and scope discipline. Browser/DOM evidence must come from learner observation; machine checks must not fabricate it.
منابع و تازگی
مطالعهٔ موارد (case studies)
فایلهای آزمایشگاه
Starter مربوط به product list را repair کنید. category/page باید در runtime validate شوند؛ query key باید هر دو dependency normalizeشده را داشته باشد؛ remote rows نباید به source of truth محلی مستقل کپی شوند؛ details dialog محلی بماند؛ بعد mutation fake API خانوادهٔ products بهدرستی invalidate شود. سپس orders transfer را مستقل حل کنید. فقط observationهایی را که واقعاً اجرا کردهاید در evidence ثبت کنید.
رسانه و اسلایدهای درس
فایلهای آموزشی (دانلود)