Skip to content

4-file folder

model/{model}/components/{view-name}/:

{view-name}/
├── index.tsx    — async container (fetch → view)
├── server.tsx   — fetch functions only (no JSX)
├── view.tsx     — display (object props)
└── loading.tsx  — Suspense fallback
FileRole
index.tsxAsync container; wires fetch + view
server.tsxfetch{Entities}, fetch{Entity}ById, etc. — no components
view.tsxOOUI UI; add 'use client' if needed
loading.tsxSkeleton; no data

pages / app imports container + loading from index only (index re-exports loading). Wraps Suspense itself. Never import server / view / loading directly from outside the folder.

tsx
// server.tsx — fetch only
export async function fetchArticles(): Promise<Article[]> { ... }

// index.tsx — container + re-export loading
export { ArticleListLoading } from './loading';

export async function ArticleList() {
  const entities = await fetchArticles();
  return <ArticleListView entities={entities} />;
}

// pages — assembly (single import path)
import { ArticleList, ArticleListLoading } from '@/model/article/components/list';

<Suspense fallback={<ArticleListLoading />}>
  <ArticleList />
</Suspense>

New views (preview/ etc.) use the same 4-file set.

OOUI props

tsx
// OK
<UserAvatar user={user} />
<ArticleCard article={article} />

// NG — object as unit of concern is lost
<UserAvatar name={name} iconUrl={iconUrl} />
  • Model components take objects (or id + loaded object) as core props
  • Split fields inside the component
  • common/ui is object-agnostic; model hooks/stores bridge object ↔ fields

Naming — no path duplication

NG  hooks/use-list-filter.ts
OK  hooks/list-filter.ts  → export function useListFilter()
KindPathSymbol
hookhooks/{kebab}.tsuse{Pascal}
constconst/{topic}.tsSTATUS_LABELS etc.
liblib/{topic}.tsfunction name
viewview.tsx{Entity}{View}View
fetchserver.tsxfetch{Entities} / fetch{Entity}ById
containerindex.tsx{Entity}{View}

Where to put code

ContentLocation
Shared across viewsmodel/{model}/hooks/, lib/, const/, query.ts
One view onlycomponents/{view}/hooks/, lib/, store/
Fetch functionsserver.tsx (no JSX)
Wire fetch + viewindex.tsx
Form statecomponents/{view}/store/

common/

KindPathExamples
Visual UIcommon/ui/TextInput, Dialog
Side effects (null render)common/effects/BeforeUnload
Logiccommon/hooks/useBeforeUnload

Hook holds logic; effect is a thin declarative wrapper. Providers with children → common/ui/theme-provider/.

Model naming

Singular kebab-case (article). URL route plural (articles).