Skip to content
OpenPerf
Esc
↑↓navigate↵open⌘Jpreview
On this page

Web apps

Rules for TanStack Start apps, currently apps/web.

Top level: components/, features/, functions/, hooks/, libs/, providers/, routes/, widgets/. @/ points at src/. File and class rules are in Frontend.

Features

A feature in features/<names>/ uses only the layers it needs. Push logic down and keep UI dumb:

Layer Holds
api/ Query contracts shared by loaders and pages, such as definePageQuery.
libs/<domain>/ Pure code without React: transforms, parsing, query keys, types.
hooks/ Stateful logic without markup.
providers/ Context and runtime wiring. Never depends on widgets.
components/<name>/ Presentational parts. Data and callbacks come through props only.
widgets/<region>/ Composed regions that call data hooks, including single-use dialogs.
pages/ Route entries.
  • Each layer exposes an index.ts; a feature has no root barrel. Outside a feature, import its layer barrel (@/features/errors/hooks); inside, import relatively.
  • functions/isomorphic/ holds .isomorph.ts modules, functions/server/ holds .server.ts ones. Client code deep-imports a .server.ts file instead of the server barrel.
  • Route-only loaders and search schemas stay in routes/. A feature’s root constants.ts and types.ts hold only symbols several layers share.
  • Keep a helper, constant or type next to its only consumer. Constants used by one function live inside it; static arrays, objects and Lingui descriptors live at module scope. Derive result types with ReturnType next to their owner.
  • Load shared data in the closest common parent and pass it down, instead of hooks that only forward it.
  • Tests sit next to the code whose behaviour they check.
  • Name mutation hooks use<Entity><Action>. Components, hooks and *QueryOptions are exempt from verb-first names.

Dialogs

  • Open a dialog with a detached trigger: useState(() => Dialog.createHandle<Payload>()), a Dialog.Trigger with that handle and a payload, and one Dialog or DialogConfirm reading payload from its render prop.
  • Don’t control dialogs with open and onOpenChange. The mobile sidebar is the one exception.

Settings

What is edited How it saves
An independent text field InlineTextField: on blur or Enter, Esc reverts, errors under the field.
An independent switch, select, checkbox On change, disabled while saving, errors in a toast through meta.errorTitle.
Fields that only make sense together One form with UnsavedChangesBar, useUnsavedChanges and useBlockUnsavedChanges.
A new entity or a list item An explicit Create or Save button, in a dialog or on its own page.
A destructive action The danger zone, behind DialogConfirm.
A state action: enable, mute, sync A button or menu in the page header.

Fields show no saving state of their own. A form with text fields confirms each save with a success toast that has one id per form, so repeated saves replace it; lists of standalone switches skip it. Creating, deleting and state actions toast their result, and a new entity opens after it’s created.

Was this page helpful?