Skip to content

Front End State

Use this page when a frontend change is mostly about state ownership, serialization, persistence, query controls, saved views, workflow draft state, or media draft boundaries.

Use Front End Components when the main question is reusable UI behavior, layout composition, page/domain wrappers, workflow hooks, labels, or component testing posture.

Use the narrowest state layer that matches the behavior.

State LayerUse For
Component-local stateOpen/closed state, focused item, draft input text, drag hover, local UI affordances.
Page frame stateCurrent view mode, selected filters before apply, active search query, active table/gallery view.
API/query cache stateServer-backed data, loading, refetching, stale data, mutation status.
URL stateReloadable and shareable browsing state such as page, limit, filters, sorts, search, view mode.
Saved-view stateDurable named browsing presets, typed by page key and schema version.
Workflow draft stateMulti-step create/edit state such as selected media files before persistence.
Auth/current-user stateSession-derived user identity, roles, permissions, and permission-aware UI checks.

Do not promote state upward just because a nearby component can see it. Promote state when another layer must coordinate it, persist it, serialize it, or use it for a side effect.

Examples:

  • Popover open state belongs inside the popover or typeahead component unless a page needs to coordinate it with another surface.
  • Search, filters, sorts, pagination, and view mode belong to the collection page because table and gallery views render the same server-backed result set.
  • Current user and permissions belong to the auth query/hooks described in Authentication And Authorization, not to local navbar or button state.
  • Relationship assignment drafts can remain page/workflow state until they need to become persisted association rows; the modeling rules live in Domain Relationships.

Keep component-local state declarations in the main component function by default. This makes the component’s changing values visible beside its render contract; extracting supporting logic does not require hiding the state declarations with it.

Around five or six useState calls is a fuzzy signal to consolidate related values into a strongly typed object with one useState call. Related subtrees of state favor consolidation sooner, especially when values are initialized, reset, or changed together. This is a judgment about coherence, not an exact count or an instruction to merge every state owner.

For example, MediaCarousel keeps its measured close-button height and viewport sizing variables in TMediaCarouselLayoutState. One visible state declaration owns that layout group while its calibration hook coordinates browser measurement and sizing. The initializer and updates retain the same runtime behavior.

Use a focused T... type for an object state group, normally in the component’s helpers module. Group fields by the UI responsibility they describe. Separate groups can remain separate when their lifecycles and ownership are independent; avoid one oversized page-state object that absorbs query results, form drafts, URL state, and workflow state.

When updating one part of an object, use a functional update if it depends on the previous state and preserve the other fields. Keep lazy initialization, reset behavior, update ordering, and equality guards intact. Concurrent callbacks must not overwrite each other’s fields with stale snapshots.

A declaration count alone does not justify introducing a reducer, changing the form library, adding a store, or moving state into another layer. Preserve existing form, query, URL, auth, and workflow owners and avoid local copies of values they already own. Use Component Module Architecture to decide which supporting logic belongs beside the state and which benefits from a helper view.

Queryable collection pages should derive available controls from endpoint capabilities in @wavemap/api-contracts.

The frontend draft model can be richer than the API model. Draft query state may carry editing-only fields such as selected option keys, condition type, UI IDs, typeahead metadata, or incomplete clauses. The apply boundary should normalize that draft into QueryFilterGroupDTO and QuerySortInstructionDTO shapes before it reaches:

  • API requests.
  • URL serialization.
  • Saved views.
  • Backend route tests.

Current page rules:

  • Search, filter, sort, page, page size, and view mode belong to the page frame, not individual table or gallery views.
  • Reset pagination to page 1 when search, filters, sorts, or page size changes the dataset.
  • Drop unsupported criteria or invalid URL values using endpoint capabilities.
  • Keep stable fallback sorting on the backend so pagination stays deterministic.
  • Keep display-only preferences out of saved views until the page exposes them as first-class controls.

For example, if an endpoint only supports filtering artists by name, genre, and city, the page may still keep a richer draft UI object for editing chips and typeahead selections. The apply boundary should remove incomplete or unsupported criteria before building the DTO sent to the API, URL, or saved-view layer. The deeper contract guidance lives in API Contracts, and the collection/query UX model lives in Query Controls And Browsing State.

Dashboard data uses semantic per-metric TanStack Query entries independently of widget identity, size, display, and order. A transient scheduler batches transport without owning another result store, while the editor retains one TanStack Form composition draft for personal views and platform curation. Applied published snapshots stay independent of background refreshes; copying preserves the displayed revision, and loading history changes the draft without moving its applied revision. See Dashboard Aggregates for publication, recovery, scope freezing, reporting-midnight reconciliation, freshness, cancellation, and Event mutation invalidation.

URL state is reloadable, shareable, and transient. Saved views are durable named presets above URL state.

Use URL state for:

  • page
  • limit
  • filters
  • sorts
  • Page-owned browsing state such as submitted search and view mode.

Use saved views when a user intentionally names and reuses a browsing setup. Saved views should be typed by page and schema version through @wavemap/api-contracts so future migrations are deliberate.

Guest users can keep saved views in local storage. Authenticated users can persist them through the page query preset API. When both sets exist after login, the import path should copy local presets to the account without deleting the device-local versions.

Keep the reusable machinery and the page adapter distinct:

  • The shared preset controller owns guest/account persistence and active-preset lifecycle.
  • The shared import registry owns page-family discovery for device-to-account import.
  • The shared popover owns save, apply, delete, loading, and error interaction.
  • Each page owns its typed durable projection, schema migration, URL-state application, query-identity invalidation, and localized labels.

Artists, Events, and Venues now use this split. A Venue saved view may retain the selected layout-only map mode, but it does not retain a map viewport, selected marker, provider state, or other runtime-only map data.

Example split:

  • A URL like ?page=2&limit=24&search=jazz&view=gallery should be enough to reload or share the current browsing posture.
  • A saved view named “Montreal venues to review” should store a typed preset for later reuse, not just a copied URL string.
  • A stale saved view should be migrated or rejected through its page key and schema version, not silently patched in the rendering component.

Workflow hooks are covered in Front End Components because they are a rendering-boundary tool: they keep page and layout components from owning request choreography. Their output is also a state contract.

Treat workflow state as the coordination layer for multi-step UI intent:

  • Draft state before persistence, such as selected media files, reordered gallery items, or relationship assignments.
  • UI-facing pending, success, partial-success, warning, and failure state.
  • Retry posture and recovery hints after a partial failure.
  • Diff state between persisted backend rows and the user’s current browser-side draft.
  • Side-effect readiness, such as when a form can submit or when the next workflow command is allowed.

Keep workflow state local to the page or workflow hook until it must cross a durable boundary. When it crosses a boundary, make the boundary explicit: API request DTO, URL serialization, saved-view schema, persisted row, or generated artifact.

Media is the clearest example of why frontend draft state must stay separate from persisted contracts.

The front end may hold:

  • Browser File objects.
  • Object URLs or local previews.
  • Upload ordering and profile/gallery intent before persistence.
  • Persisted media already loaded from the backend.
  • Pending removals, reorders, and alt-text edits.

The backend owns persisted media records, canonical locator fields, ingest, thumbnails, blur previews, and render-ready URLs. Frontend code should not treat temporary browser draft state as the same thing as artist_media records or media DTOs.

Generic upload components should keep UI behavior reusable. Artist-specific wrappers and workflows should decide how draft items become artist-media uploads, sync requests, warnings, or recovery hints.

Treat object URLs, drag order, selected files, and unsaved alt text as browser draft state. Treat storageLocation, thumbnailStorageLocation, public render URLs, lifecycle status, and metadata as backend-owned persisted state. The durable storage, ingest, delivery, and deployed-dev boundaries live in Media Storage And Delivery.