Skip to content

Front End Components

Use this page when a frontend change is about rendering structure, reusable UI behavior, page/domain adaptation, workflow sequencing, or component copy.

Use Front End State when the main question is where state should live, how it should serialize, or how URL, saved-view, query-cache, workflow draft, and media draft state relate.

Reusable components should be generic enough to survive outside their first Wavemap screen.

Good reusable component responsibilities:

  • Input wiring and controlled/uncontrolled value behavior.
  • Popover, sheet, dialog, carousel, table, upload, pagination, and selection mechanics.
  • Keyboard and accessibility behavior.
  • Stable sizing, empty/loading/error states, and option rendering hooks.
  • English defaults when the component is useful outside a localized Wavemap surface.
  • Grouped labels props when the component owns internal visible copy across several subcomponents.

Avoid giving reusable components responsibility for:

  • Fetching domain data.
  • Knowing API DTO shapes.
  • Importing Wavemap translation namespaces or calling t().
  • Owning page URL state.
  • Applying route permissions.
  • Choosing page-specific toast, form, or navigation side effects.

Examples of this split:

  • TypeaheadSearch owns the larger input-plus-results presentation.
  • CompactTypeaheadSearch owns combobox and popover mechanics for smaller search fields.
  • Table, filtering controls, SortAndFilterPanel, Pagination, FileUploadSurface, Carousel, MediaCarousel, Breadcrumbs, and date/time/numeric inputs receive labels and formatters instead of owning Wavemap translation keys.

A good reusable component should be testable with plain props and simple fixtures. For example, a compact typeahead can be tested with three static options, a selected value callback, and custom labels; it should not need an artist API mock just to prove keyboard selection. When a component needs many labels, keep the label-map pattern aligned with i18n Contributor Workflows.

Layout composition components sit between reusable primitives and full page/domain wrappers. They are useful when a page has enough structure to extract, but the extracted structure is still too specific to become a design-system component.

They own:

  • Page section arrangement, responsive grid or stack structure, and repeated page chrome.
  • Placement of prepared cards, panels, forms, sidebars, tabs, headers, toolbars, and empty/error regions.
  • Visual grouping of already-loaded or already-derived domain data.
  • Wiring between generic child components when the wiring is presentational rather than data-fetching.
  • Domain-adjacent names when they make the page easier to read, such as ArtistDetailsHeader or ArtistMediaSection.

Avoid giving layout composition components responsibility for:

  • Fetching data or calling API hooks.
  • Owning URL state, saved-view state, or query cache state.
  • Translating DTOs into view models.
  • Resolving permissions or choosing workflow side effects.
  • Importing page-specific translation namespaces unless the component is intentionally a page-local extraction.
  • Becoming a reusable component library candidate before it has more than one real consumer.

The goal is to keep page files from becoming long render trees without pretending every extracted region is a reusable UI primitive. A page can load data, derive display-ready props, and hand them to layout composition components that arrange the surface. Those layout components then hand generic behavior to reusable children such as tables, typeaheads, carousels, upload surfaces, switches, buttons, and form controls.

Useful examples:

  • ArtistDetailsHeader can arrange the artist title, metadata, status actions, and permission-aware action slots after the page wrapper has already loaded the artist and resolved the allowed actions.
  • ArtistMediaSection can arrange profile image, gallery, empty media state, and edit affordances while still delegating carousel, upload, and button mechanics to reusable components.
  • RelationshipAssignmentLayout can arrange selected candidates, search results, pagination, and footer actions while a page or workflow hook owns the assignment draft and persistence sequence.
  • SearchResultsFrame can arrange filter chrome, result count, view switcher, table/gallery regions, and empty states while the page frame owns the query state and result mapping.

Page/domain wrappers should adapt reusable components and layout composition components to Wavemap behavior.

They own:

  • API hook invocation.
  • Debounce, fetch, and loading state for domain data.
  • DTO mapping into render rows, options, cards, or table data.
  • Query-control capability selection.
  • Localized copy, grouped labels, and formatter functions.
  • Result selection behavior.
  • Route pushes, URL serialization, and saved-view persistence.
  • Permission-aware rendering and page-specific empty/error states.

This is why typeahead and search surfaces should usually be parent-driven. The parent or domain wrapper owns the query, fetching, filtering/ranking if any, result mapping, and selection side effect. Layout composition components can arrange the search region and results region. The reusable search component owns how that already-curated state is displayed and operated.

Useful examples:

  • Artist search wrappers can call the relevant query hook, map artist DTOs into option labels and thumbnails, and pass result actions into a generic typeahead.
  • Relationship sheets can keep assignment, pagination, selected candidates, and permission-aware actions near the domain workflow while using generic sheet, search, list, and pagination pieces.
  • Admin or edit pages can combine route guards from Authentication And Authorization with page-local empty and unauthorized states instead of making reusable components know about roles.

A component’s main function should make its state and JSX easy to understand together. Keep ordinary setup visible when it helps the reader: props, state, refs, constants, small functions, and memoized computations can all belong in the main body.

Review extraction when setup before the main JSX return becomes a slog. Roughly 100–150 lines is a fuzzy cognitive signal, not a target, limit, or lint rule. Dense coordination can justify extraction sooner; a clear prop list or declarative render tree can remain longer. The useful question is whether a reader can understand the rendered outcome and inspect supporting logic separately when needed.

Declare component-local state in the main component by default. Around five or six useState calls is a fuzzy signal to consolidate related values into a strongly typed object; related subtrees of state favor consolidation sooner. Keep genuinely distinct owners separate. See Component-Local State for grouping and update semantics.

Keep straightforward refs, variables, constants, callbacks, useMemo, and useCallback calls alongside that state when they remain easy to scan. Extract substantial style chaining, derived structures, coordination, or imperative lifecycles when they obscure the JSX. State declarations can remain visible while a calibrator receives the state, setters, and refs needed to coordinate those operations.

Do not route ordinary props or state through a helper merely to return them unchanged. Use props directly at JSX call sites and return derived resources or commands whose preparation benefits from a separate view.

Use only files that earn a meaningful responsibility:

Component/
Component.tsx
helpers.ts
ComponentStyles.module.scss
components/
__tests__/
ModuleOwns
Component.tsxProps, visible component-local state and straightforward setup, an optional calibration call, and JSX composition.
helpers.ts or helpers.tsxSupporting types, constants, builders, adapters, label maps, component-local hook logic, and the optional calibrator. Use TSX only when helpers contain JSX.
components/Substantial component-local regions with semantic names and independently understandable props.
__tests__/Public behavior at the layer that owns the risk.

Prefer helpers.ts for component-local support instead of creating parallel hooks.ts and i18n.ts files. Keep genuinely curated subsystem, workflow, translation-runtime, or server/client boundaries in their established modules when those boundaries serve independent ownership or safe imports.

Before merging companions, trace their consumers. A server route that imports a pure label or query-key builder must remain server-safe. Moving client-only imports into that builder’s module can change its import graph even if the builder itself is pure. Preserve a curated boundary when necessary, and record why it exists. Adding "use client" to a general helper module is not a substitute for resolving that ownership.

A component may have one calibrating function when its preparation is substantial enough to benefit from a separate view. Choose its name from what that function contains:

  • calibrateComponent when the function is hook-free.
  • useCalibratedComponent when the function calls React hooks or other hooks, including useMemo or useCallback.

Do not compose calibrateComponent inside useCalibratedComponent, or create equivalent controller and presentation calibrators under different names. They are alternative names for one boundary. Hooks used directly in the component do not force its otherwise pure calibrator to become a hook.

A pure calibrator can resolve presentation defaults, class names, styles, ARIA values, and derived display resources. It must remain free of hook calls and side effects. A hook-backed calibrator can additionally coordinate substantial effects, memoization, browser integration, or existing subsystem hooks. Keep hook calls unconditional and preserve their order and lifecycle semantics.

Focused builders can remain independently named when they express a real transformation, such as resolving viewport sizing or mapping an API summary to a table row. The component may call a useful builder directly. Neither trivial setup nor every pure helper call requires a calibrator.

Group returned resources when the group has a meaningful UI responsibility, such as derived labels, layout, or navigation. Keep the return contract small enough to follow. Avoid opaque bags, large forwarding types, and groups created solely to repackage existing props or state.

The Wavemap-local MediaCarousel demonstrates this balance: its main function owns typed layout state, refs, defaults, and small callbacks; one useCalibratedComponent in helpers.ts coordinates sizing, measurement, focus, and derived presentation. Its deterministic sizing resolver remains a named utility. The amount of preparation that stays local should still be judged for each component.

Preserve established owners:

  • URL state and saved-view helpers remain the durable browsing-state adapters.
  • Endpoint query and mutation hooks retain transport and cache ownership.
  • Workflow hooks coordinate mutations, partial success, retry, revisions, and recovery.
  • Shared imperative hooks can own measurement, observers, focus restoration, timers, or DOM integration.

Call these hooks from the main body or the single hook-backed calibrator according to the component’s complexity. Their existence does not require another calibration layer. Keep separately named hooks when they provide a reusable subsystem or a contract that deserves independent ownership and testing.

Memoization should serve computation cost, reference identity, or a child/effect contract. Moving code does not by itself justify adding or removing memoization. Preserve dependency arrays, functional state updates, lazy initialization, cleanup, and closure behavior during a placement-only refactor.

ConcernUse
LocalizationPage/domain wrappers acquire translations in the main body or their hook-backed calibrator and prepare display-ready label maps. Generic components receive labels and formatters. Component-local label builders normally live in helpers.
Authentication and authorizationProtected pages use useRequireAuth(); public surfaces use permission hooks or RequiresPermission. Generic components do not inspect roles.
Animation and navigationUse shared motion tokens, variants, and transition hooks. Preserve real-link semantics and reduced-motion behavior.
Server dataEndpoint hooks retain transport and cache ownership. Pure calibrators and layout composition components do not fetch data.
FormsPreserve the established form library and single draft owner. Keep mutation, media, relationship, revision, and navigation workflows at their existing boundary.
StylesKeep static styles in the owning style module. Straightforward computed styles can stay local; substantial chaining or calculation can move to helpers or the calibrator.

Use Runtime And Frontend, Authentication And Authorization, and Front End State for their respective ownership rules.

Check one representative slice before propagating a structure across a component family. Collection pages, detail pages, reusable interaction components, auth flows, and authoring forms have different ownership pressures.

For each slice, identify:

  • The component role and its query, state, effect, i18n, auth, motion, form, and test owners.
  • The state and straightforward setup that should remain visible.
  • The support logic that merits extraction, the calibrator’s name if needed, and any retained special module boundary.
  • The actions that should remain understandable at their JSX call sites.
  • The public behavior, accessibility, and responsive expectations that must remain unchanged.

Keep Wavemap-local routing, maps, forms, providers, auth, locale switching, app shells, and domain adapters local unless a separate promotion lane defines a reusable public contract. For installed Codon UI components, correct the canonical source and use the guarded registry workflow to consume the change.

Endpoint hooks should stay close to individual API routes. Feature-level workflow hooks should own sequencing.

Workflows live on this page because they keep components and page wrappers focused on rendering prepared state. They also coordinate workflow draft state, pending state, partial-success state, retry posture, and recovery hints, so keep their state outputs aligned with the ownership rules in Front End State.

Use endpoint hooks for:

  • One API route.
  • Request args and response typing.
  • Mutation/query options close to the transport boundary.

Use workflow hooks for:

  • Creating a parent entity and then attaching related media.
  • Editing details separately from media collection sync.
  • Diffing retained, reordered, deleted, or promoted media.
  • Add-mode draft relationship fan-out after entity creation.
  • Retry posture, warnings, and recovery handoff.
  • UI-facing pending, success, partial-success, and failure state.

Avoid placing request choreography directly in large page components. Page components should read workflow state, render the form or surface, and call clear workflow commands.

For example, Add Artist can use endpoint hooks for creating the artist and uploading media, while a workflow hook owns the sequence: validate draft media, create the artist, attach selected images, choose the profile image, surface partial success, and hand recovery hints back to the page. Edit Artist can reuse the same media primitives but needs a different diff: retained persisted media, new browser files, deleted items, reordered gallery images, and profile-image changes. The media proof and deployment lanes for those flows live in Media Workflow And Validation.

Reusable components receive display-ready labels and formatter functions.

Use grouped label maps when a component has internal visible copy across several subcomponents and areas, such as:

  • Primary controls.
  • Status text.
  • Empty/loading/error text.
  • Row or item actions.
  • ARIA labels.
  • Dynamic formatter functions for counts, positions, pages, slide labels, and toast bodies.

The grouping should mirror the component’s internal label supply chain. Resolve labels once near the top-level component or page/domain adapter, then pass complete groups to subcomponents. Keep page or domain namespaces for workflow-specific copy, and use common--* namespaces for product-neutral component chrome.

Do not pass Wavemap translation keys, namespace names, or t() into generic reusable components.

Good shape:

const paginationLabels = {
controls: {
nextPage: t("pagination.next"),
previousPage: t("pagination.previous"),
},
status: {
pageSummary: ({ page, totalPages }) => t("pagination.summary", { page, totalPages }),
},
}

The page or domain wrapper resolves t() once, then the reusable component receives display-ready strings and formatter functions. Use Translation Resources for key and sheet conventions, and i18n Guardrails for verification boundaries.

Choose the layer that owns the risk:

  • Reusable component tests should cover props, default labels, custom label maps, formatter behavior, and meaningful interactions.
  • Page integration tests should cover loading, empty, error, search, filter, sort, view switching, saved views, and workflow state preservation.
  • Hook tests should cover API serialization, query keys, diffing, sequencing, partial-success behavior, and retry posture.
  • URL-state tests should cover parsing, sanitization, serialization, unsupported criteria, and invalid limits.
  • Browser smoke should stay narrow and prove behavior that only exists in a browser, such as routing, focus restoration, media rendering, or deployed runtime behavior.

Avoid testing calibration helpers or implementation-only plumbing when behavior is already covered through the public component or workflow surface.

Examples:

  • Test a reusable table filter popover with static column definitions, custom labels, and keyboard interactions.
  • Test a page wrapper by mocking the endpoint hook or API layer and asserting that URL state, empty states, and view switching survive user actions.
  • Test a workflow hook around the sequence and resulting states, especially partial success and retry posture.
  • Save browser smoke for behavior that needs the browser runtime, such as focus, routing, media rendering, or deployed edge/runtime integration.

Use Testing Overview for the full layer-selection rules and Local Development Runtime for the local browser runtime setup.