وب مدرن، رابط کاربری و تجربه‌ی وب · فصل ۴ — React، State و Visual Regression · 46

Server State and Typed Routing in React

Separate local, URL and server state; bind query identity to the data dependency, define freshness/invalidation behavior, and combine typed routing with runtime search-param validation so route and cache evidence remain inspectable. می‌تواند «Server State و Typed Routing در React» را در یک feature محدود طراحی/پیاده‌سازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.

Server State و Typed Routing در React — fa-IR

Lesson summary

Separate local, URL and server state; bind query identity to the data dependency, define freshness/invalidation behavior, and combine typed routing with runtime search-param validation so route and cache evidence remain inspectable.

می‌تواند «Server State و Typed Routing در React» را در یک feature محدود طراحی/پیاده‌سازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.

This lesson produces

می‌تواند «Server State و Typed Routing در React» را در یک feature محدود طراحی/پیاده‌سازی کند و correctness، accessibility یا failure behavior مرتبط را اثبات کند.

Who it is for

Foundation

Worked baseline, explicit evidence table, safe local fixture and one guided failure probe.

Repair

Reduce to one failing contract, observe evidence, repair, then restore full case.

Experienced

Skip syntax walkthrough; require independent implementation, mutation evidence and residual-risk statement.

Prerequisites

A correct-looking product list can still have the wrong state contract

A React screen can look completely normal while three different kinds of state are silently mixed together. A category filter may live in component state even though it should be bookmarkable. Remote product data may be copied into local state even though another system owns the authoritative record. A query key may say only products even though the result changes with category and page. A route API may be strongly typed in TypeScript while malformed search values still enter at runtime. WEB-012 is about making those boundaries explicit and then proving them with evidence.

The bounded scenario is a searchable product list. Category and page must survive refresh and sharing, so they belong in URL/search state. The details dialog is temporary UI memory and stays local. Product rows are remote data and remain server state. The query identity must include every dependency that changes the returned list. A successful mutation must invalidate the relevant query family, but invalidation is not described as proof that fresh bytes have already arrived. Finally, raw search input is validated at the runtime boundary before the application trusts typed values.

Completion means producing Server-State & Typed Route Contract Record v1. That record separates implementation facts, observed behavior, inference, failure mutation, repair, transfer, and residual limitation. A visually correct list is not enough.

Scope, route, and evidence bar

This lesson builds directly on WEB-012. The prerequisite idea is simple: component-local state is memory owned by a React component tree. WEB-012 adds two other ownership domains. URL state is navigation state whose useful property is persistence across refresh, back/forward navigation, bookmarks, and shared links. Server state is asynchronously owned remote data that a client may cache, refetch, invalidate, and temporarily display but does not become authoritative merely because it is cached.

The lesson does not claim that TanStack Query or TanStack Router is mandatory for every React application. They are used as a concrete, current fixture because their APIs make the boundaries inspectable. The durable skill is the contract: classify state, make remote identity complete, define freshness and invalidation behavior, carry route types through registered route context, and validate untrusted URL/search input at runtime.

Use the foundation route if query caches and route trees are new. Use the repair route if you recognize the APIs but confuse cache data with source-of-truth data or compile-time typing with runtime validation. Use the experienced route if you can skip syntax and go directly to identity, lifecycle, and evidence. All routes end at the same transfer task.

Mental model: raw URL → validated route state → query identity → remote lifecycle → rendered evidence

The core model is a pipeline with explicit boundaries.

  1. Raw URL/search input is user-facing input. Treat it as untrusted.
  2. Runtime validation parses or rejects/defaults that input according to an explicit schema or validation function.
  3. Typed route state is the validated value exposed through the registered route APIs.
  4. Query identity includes every validated remote-data dependency that changes the result, such as category and page.
  5. Fetch/cache lifecycle may serve cached data, fetch, become stale, refetch, or be invalidated according to policy.
  6. Mutation and invalidation express that related cached knowledge is stale and should be reconciled according to the query client's behavior.
  7. Rendered evidence is observed separately from the architectural inference you make from it.

The details dialog sits beside this pipeline rather than inside it. selectedProductId may be ordinary local state because opening a dialog is not necessarily a bookmarkable navigation requirement. If product selection itself must be linkable in a real product, that requirement would change the classification; the mechanism follows the product contract, not a slogan.

Foundation: five contracts that must remain distinct

1. State-classification contract

Ask who owns the truth and what persistence the user expects. Temporary UI memory usually belongs locally. Shareable navigation choices belong in the URL. Asynchronously owned remote records belong to a server-state mechanism. This prevents a common architecture smell: one giant client store containing dialog visibility, route filters, and copied server rows with no ownership distinction.

The practical test is to change one dimension. Refresh the page. Copy the URL into a new tab. Refetch the server data. Close and reopen the dialog. If a piece of information survives or changes at the wrong boundary, the classification is probably wrong.

2. Query-identity contract

A query key is not a label for a component; it is identity for a cached remote-data dependency. If the fetch result changes when category or page changes, those values belong in the identity. ['products'] for every filtered page allows distinct route states to collide into one cache slot. A stronger identity is ['products', { category, page }] or an equivalent stable representation.

Completeness matters more than string style. The evidence should show two different normalized route states and two different query identities. Then deliberately remove one dependency and demonstrate the collision condition that the stronger key prevents.

3. Freshness and invalidation contract

Cached data is a client-side observation of remote state. Its lifecycle needs explicit rules. Current TanStack Query guidance treats invalidation as marking matching queries stale and, for active matches, typically triggering background refetch behavior. Therefore the claim must stay narrow: invalidation changes cache lifecycle state; it is not itself proof that the remote system has already returned newer data.

After a successful mutation, identify the affected query family and invalidate that family. If the mutation changes one product that can appear across category/page views, an appropriate prefix may be broader than one exact page key. The required breadth depends on the data dependency, not on convenience.

4. Typed-route-context contract

TypeScript can carry route information through route definitions, links, navigation calls, and route hooks when the application uses the registered route tree/context correctly. This reduces accidental mismatches during authoring and refactoring. It does not remove runtime boundaries. Types describe what the compiled program expects; URLs still arrive from outside the type system.

In the lab, links/navigation use the registered /products route contract rather than inventing unrelated string parsing in each component. This keeps the route state definition in one inspectable place.

5. URL runtime-validation contract

Search params are user-controlled input. Validate them before trusting them. The fixture normalizes page to a positive integer and category to a permitted set. Malformed values do not become valid merely because a developer writes as ProductSearch.

A useful failure probe replaces runtime validation with a type assertion and then feeds malformed input. The code may still satisfy the compiler in a narrow location, but the runtime evidence exposes the false guarantee.

Deep explanation: why plausible shortcuts create hidden bugs

The most dangerous mistakes are locally plausible. Copying fetched rows into useState feels convenient because local edits become easy. But now there are two independently changing representations: query data and component state. A refetch can update one while the screen continues reading the other. If editing requires a draft, keep the draft intentionally local and scoped; do not clone the entire remote collection as a second source of truth.

An incomplete query key is similarly quiet. The first view loads correctly. The bug appears only when another route state asks for a different result under the same identity. Without a deliberate changed-state probe, the screen may appear correct throughout development.

Type assertions create another kind of quiet failure. raw as ProductSearch tells TypeScript to trust the programmer. It does not inspect the URL. Runtime validation is an executable boundary, not a type annotation.

Invalidation also invites overclaiming. A call to invalidateQueries can be awaited, and mutation callbacks can coordinate promises, but your evidence should distinguish the API event from the actual application observation. Record when invalidation was requested, which key family was targeted, what loading/refetch state was observed if available, and what rendered data was eventually observed. Do not compress all of that into “invalidate means fresh.”

Worked example: bookmarkable filters, local dialog, remote products

Start with /products?category=books&page=2. The validator produces a normalized route state such as { category: 'books', page: 2 }. The query uses that exact dependency in its identity. The fetch function receives the same normalized values. Opening a product details dialog sets a local selectedProductId; it does not rewrite the route because dialog openness is not part of this scenario's sharing contract.

Now navigate to category electronics while keeping page valid. The route state changes and the query identity changes. The cache can therefore distinguish the two remote dependencies. If you navigate back, a cache may be reused according to freshness policy, but the application still treats the remote system as authoritative.

Next, run a mutation that changes a product name. On success, invalidate the products family that can contain that item. The evidence record notes that invalidation was issued and then separately records the later observed list. This separation matters because background work, errors, inactive queries, and configuration can affect when a visible update occurs.

Finally, refresh or share the URL. Category and page survive because they live in navigation state. Dialog openness disappears because it is local. That is not data loss; it is the intended ownership contract.

Failure probes: the evidence must react when the model breaks

Mutation A — remove category/page from the query key

Change the key to ['products']. Predict a collision: two different route states now refer to one cache identity. Run the checker and changed-state case. The expected diagnosis is not “TanStack Query is broken”; the dependency identity is incomplete.

Mutation B — replace runtime validation with a cast

Replace the search validator with an assertion or unvalidated pass-through. Feed ?category=unknown&page=-4. If the application accepts those values as trusted route state, TypeScript did not validate the runtime input. Repair by restoring executable parsing/default/rejection behavior.

Mutation C — invalidate the wrong query family

After the mutation, invalidate ['product'] while the list queries are under ['products', ...]. The mutation can succeed remotely while the visible cached list remains stale. Repair the invalidation target and rerun the same observation.

Mutation D — copy fetched rows into independent local state

Initialize const [rows, setRows] = useState(query.data ?? []) and render rows as the primary list. Then refetch. Query data and rendered local copy can diverge. Repair by rendering query data directly and isolating only genuinely local draft/UI state.

Experienced bridge: reason in ownership and identity, not library slogans

For an experienced engineer, the durable question is not “Should I use a query library?” It is “What system owns this value, what identity determines the data, what boundary validates external input, and what observation would falsify my model?” A library can encode good defaults, but it cannot decide your product semantics.

Treat query keys as a projection of remote dependencies. Treat URL schemas as boundary contracts. Treat invalidation as a lifecycle operation. Treat local state as intentionally local. This vocabulary scales to other routers and caching clients without pretending the APIs are identical.

When reviewing code, look for evidence of hidden duplication: route filters mirrored into component state, server rows cloned into a client store, multiple parsers for the same search params, or mutation code invalidating an unrelated prefix. These are signals to restate the contract before adding more abstractions.

Alternatives and trade-offs

A small application can use URLSearchParams, a custom fetch hook, and explicit request state instead of TanStack Router/Query. That can be appropriate when the feature is genuinely small and the team can keep identity, cancellation, freshness, validation, and sharing behavior explicit. The trade-off is that more lifecycle policy becomes application code.

A router with validated search params centralizes navigation state and can improve refactor safety. A query cache centralizes remote dependency identity and lifecycle. Neither automatically proves product correctness. Additional abstractions are useful when they make contracts easier to express and test, not merely because they are popular.

For mutations, exact cache updates can reduce refetching when the server response is authoritative and the affected cache shape is known. Invalidation can be simpler and safer when many views may contain the changed record. The lesson does not prescribe one universal strategy; it requires that the chosen strategy match the dependency and that the evidence state what it proves.

Practice: repair the fixture, then transfer the method

Guided repair

Open lab/WEB012-LAB1/learner/starter. Before changing code, complete the first contract rows: which values are local, URL, and server state; which dependencies change the product result; what malformed URL should do; and what query family a mutation affects.

Repair runtime search validation first so later evidence uses normalized route state. Then construct the query key from the normalized category and page. Remove the independent copy of query results from component state. Keep the dialog selection local. Finally, invalidate the relevant products query family after the deterministic fake API mutation.

Run the public checker. It checks structure and evidence schema; it does not certify browser behavior. Record actual browser/DOM observations only if you ran them. The package deliberately initializes the attestation as false so the learner cannot inherit fabricated evidence.

Independent transfer

The transfer fixture changes the context to an order list with status/page route state, an order details dialog, and a remote mutation. Re-derive the state classification and query identity. Do not copy the product solution token-for-token. The same method should produce a new route schema, new key dependencies, correct invalidation target, and separate local dialog state.

The transfer is complete only when the evidence record states one residual limitation. For example, a deterministic local fake API does not prove behavior under real network races, authorization changes, or multi-client server updates.

Verification: what the contract record must prove

The record has six required dimensions. Implementation artifact identifies the relevant route, query, mutation, and local-state code. Observable behavior records what was actually seen, without inventing browser evidence. Failure mutation result includes a prediction and an observed failure. Repair result reruns the same probe after repair. Changed-context transfer shows the method worked when the nouns changed. Residual limitation prevents the artifact from turning a bounded lab into a universal claim.

Keep observation and inference separate. “The URL after navigation was ?category=books&page=2” is an observation. “Therefore the router preserves bookmarkable filter state” is an inference supported by that observation in the tested fixture. “This will work in every environment” is an unsupported generalization.

The hidden oracle checks architectural contracts that are easy to spoof with comments. Passing it is necessary for the lab package but still not evidence of unobserved browser behavior.

Assessment and completion

Retrieval: classify local, URL, and server state and state one non-goal. Application: repair the product list and complete the record. Diagnosis: predict and run one deliberate failure. Decision: defend one identity/invalidation choice against a plausible alternative. Transfer: solve the order-list variant independently.

Blocking failures include an undetected critical mutation, treating visual success as the only evidence, omitting a remote dependency from query identity, trusting a type assertion as runtime validation, or claiming invalidation itself proves new server data arrived.

Completion is not “the page rendered.” Completion is an inspectable state contract plus evidence that reacts when the contract is deliberately broken.

Sources and freshness boundary

This lesson is freshness-sensitive. The external facts are deliberately narrow and tied to current official TanStack documentation. TanStack Query documentation describes query invalidation as marking matching queries stale and refetching active matches according to configuration. TanStack Router documentation distinguishes type propagation from runtime validation of user-facing search input. These facts were rechecked on 2026-09-13 and should be reviewed again by the lesson's due date or sooner if the APIs/guidance change.

The stable method is broader than the tools: classify ownership, validate external input at runtime, make dependency identity complete, define lifecycle behavior, and record evidence without overclaiming.

Assessment

There is no answer key in this lesson pack; answer it yourself and mark honestly. "Needs review" sends the lesson to your review queue.

WEB-012-S1 retrieval

Classify dialog selection, category/page, and product rows as local, URL, or server state. State one non-goal.

lesson ✗AI ✗notes ✗

WEB-012-S2 application

Repair the product route so search params are runtime-validated, query identity includes category/page, remote rows are not copied into local state, and the mutation invalidates the correct products family.

lesson ✗AI ✗notes ✗

WEB-012-S3 diagnosis

Predict and run one query-key, validation, invalidation, or server-state-copy mutation; diagnose and repair it.

lesson ✗AI ✗notes ✗

WEB-012-S4 decision

Defend the chosen query-key and invalidation breadth against one plausible alternative without claiming a universal rule.

lesson ✗AI ✗notes ✗

WEB-012-S5 transfer

Transfer the method to the order-list fixture with status/page URL state, local details dialog, and remote order mutation.

lesson ✗AI ✗notes ✗
Assessment rubric

Critical dimensions:

  1. Contract Correctness — State classification, query identity, invalidation semantics, typed route context and runtime URL validation preserve the bounded lesson contract.
  2. Evidence Quality — Observations, mutation, repair and transfer evidence are traceable and do not infer runtime facts from types or appearance alone.
  3. 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.
  4. 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.

References & freshness

· TOOL_VOLATILE
TanStack Query defines a query as a declarative dependency on an asynchronous data source tied to a unique key used for caching/refetching/sharing.
· TOOL_VOLATILE
Current TanStack Query defaults treat queries as stale by default and can refetch on lifecycle events; staleTime/gcTime/retry behavior is configurable.
· TOOL_VOLATILE
Invalidating a query marks it stale and active matching queries may refetch in the background; invalidation is not itself proof that new data has arrived.
· TOOL_VOLATILE
TanStack Router propagates registered route types through route APIs within TypeScript compiler/runtime limits.
· TOOL_VOLATILE
TanStack Router documents search params as user-facing raw input that should be validated before the application relies on typed values.

Case studies

Lab files

Repair the product-list starter. Required contracts: runtime-validate category/page; query key must include both normalized dependencies; render remote rows from query data instead of an independent copied state; keep the details dialog local; invalidate the products query family after the fake remote mutation. Then solve the unsolved orders transfer independently. Complete the evidence record and only set browser/DOM attestation to true for observations you actually performed.

Lesson media & slides

MAKNOW_WEB-012_DECISION_EVIDENCE_TABLE_v1.0.0.svg
DECISION EVIDENCE TABLE v1.0.0.svg
MAKNOW_WEB-012_FAILURE_REPAIR_v1.0.0.svg
FAILURE REPAIR v1.0.0.svg
MAKNOW_WEB-012_PRIMARY_MODEL_v1.0.0.svg
PRIMARY MODEL v1.0.0.svg
MAKNOW_WEB-012_WORKED_EXAMPLE_v1.0.0.svg
WORKED EXAMPLE v1.0.0.svg

Teaching files (download)

XLSXServer State Typed Route Contract Record v1 v1.0.0.xlsxTeaching files (download)
DOCXServer State Typed Route Contract Record v1 WORKSHEET v1.0.0.docxTeaching files (download)
PDFServer State Typed Route Contract Record v1 WORKSHEET v1.0.0.pdfTeaching files (download)