Files
template-react/CONVENTIONS.md
T
pastilhasandClaude Opus 5 c08dab4243 Initial commit: Bun · Hono · React SPA · Postgres/Drizzle template
The starting point for an app with a React frontend. Same server, env,
database and API skeleton as template-htmx — the two differ only in what is
served at / — with a React 19 SPA, Tailwind v4 and shadcn/ui on top.

Carried from a working project rather than designed in the abstract, and
carrying the lessons that cost it time:

- The dev script states NODE_ENV=development rather than inheriting it. React
  resolves its runtime against NODE_ENV: Bun emits jsxDEV() in development
  and React's production jsx-dev-runtime defines jsxDEV as void 0. Inherit
  production while transforming for development and the app dies at first
  paint. Verified: the dev bundle resolves the development runtime and
  contains no void-0 stub.
- Asset URLs in components are relative and <base href="/"> is in the HTML,
  so a path-prefix proxy can rewrite one attribute and every asset follows. A
  test enforces it.
- Routing is in the hash, because the app may be mounted under a prefix it
  only learns at runtime and a hash means the same thing at either mount.
- index.html holds __PUBLIC_URL__ and gen-index.ts substitutes it, because
  Bun's HTML bundler fails the build on a root-relative favicon href.
- APP_ENV failing closed, env files before the ambient environment, the
  database enforced by name, one Postgres client per process — as in the
  htmx template.

The database has no tables. The App is a shell with one tab and no features,
named as a placeholder. Renaming `app` is the first step and the README lists
every place it appears.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 02:02:52 +00:00

309 lines
9.3 KiB
Markdown

# Code Conventions
This document outlines the coding patterns and conventions used in this project.
A living document — adjust it as the project's needs change.
## Component Organization
### Feature Folder Pattern
Components that represent a feature or screen should use a folder structure with a barrel export.
```
Feature/
├── index.tsx # exports from Feature.tsx
├── Feature.tsx # main component
├── SubComponent.tsx
└── utils.ts
```
**index.tsx:**
```tsx
export * from './Feature';
```
**Rationale:** Keeps imports clean (`import { Feature } from './Feature'`) while allowing the feature to grow into multiple files without changing import paths. Subcomponents that only serve this feature live in the same directory rather than being abstracted to a shared components folder.
### Types Alongside Components
Export component prop types from the same file as the component.
```tsx
export type FeatureProps = {
value: string;
onChange: (value: string) => void;
};
export const Feature = ({ value, onChange }: FeatureProps) => {
// ...
};
```
**Rationale:** Keeps types discoverable and colocated with their usage. Consumers can import both the component and its types from the same path.
## State Management
### Manager Pattern for Hooks
Hooks that manage complex state should return a "manager" object. Components receive this manager as a prop.
```tsx
// Hook
export const useFeatureManager = () => {
const [state, setState] = useState('');
const [filter, setFilter] = useState('');
const filteredItems = (() => {
// derived state computation
})();
return {
state,
setState,
filter,
setFilter,
filteredItems,
};
};
export type FeatureManager = ReturnType<typeof useFeatureManager>;
// Parent component
const Parent = () => {
const manager = useFeatureManager();
return <Child manager={manager} />;
};
// Child component
const Child = ({ manager }: { manager: FeatureManager }) => {
const { state, filteredItems } = manager;
// ...
};
```
**Rationale:** Centralizes state logic in one place. Child components don't need to know about individual state setters - they just receive the manager. Makes refactoring easier since state shape changes only affect the hook.
### State Colocation in Hooks
All local state (search, filters, pagination, expanded states) should live in the feature's hook, not scattered across components.
```tsx
// Good
export const useFeatureManager = () => {
const [search, setSearch] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [expandedId, setExpandedId] = useState<string | null>(null);
// ...
};
// Avoid
const Component = () => {
const [search, setSearch] = useState(''); // state in component
// ...
};
```
**Rationale:** Single source of truth for feature state. Easier to understand, test, and modify. Prevents state synchronization bugs.
### Derived State in Hooks
Computed/derived values should be calculated in the hook, not in components.
```tsx
export const useFeatureManager = () => {
const [items, setItems] = useState([]);
const [filter, setFilter] = useState('');
// Derived state computed in hook
const filteredItems = (() => {
return items.filter((item) => item.name.includes(filter));
})();
const itemCount = filteredItems.length;
return { items, filter, setFilter, filteredItems, itemCount };
};
```
**Rationale:** Components stay focused on rendering. Business logic stays in one place. Derived values are computed once and shared across all consuming components.
## React Patterns
### Reach for useMemo / useCallback only when they do work
Default to writing the code plainly. Most derived values are cheap and re-computing them per render costs
less than the memo that guards them.
```tsx
// ✅ Fine — cheap, so just write it
const filteredItems = items.filter((item) => item.active);
const handleClick = () => doSomething();
```
But they are ordinary tools, not forbidden ones. Use them where they earn it:
- a value or callback in a **dependency array**, where an unstable identity re-runs an effect or
re-subscribes a socket every render;
- a genuinely **expensive** computation over a large list;
- a prop passed to a **memoized** child.
```tsx
// ✅ Earns it — an unstable callback here would re-subscribe on every render
const onConnectionChange = useCallback((state) => setConnState(state), [setConnState]);
```
The React Compiler is a separate, opt-in build plugin and is **not installed here** (there is no
`babel-plugin-react-compiler` in `package.json`); React 19 on its own memoizes nothing.
### Computation Functions Outside Components
Extract complex computations into functions declared below the component, not as IIFEs inside.
```tsx
export const Component = ({ data }: Props) => {
const metrics = computeMetrics(data);
const stats = computeStats(data, metrics.total);
return <div>{/* ... */}</div>;
};
// Functions below component
function computeMetrics(data: Data[]): Metrics {
// complex computation
}
function computeStats(data: Data[], total: number): Stats {
// complex computation
}
```
**Rationale:** Keeps the component body focused on rendering logic. Functions are testable in isolation. Easier to read and understand the component's purpose.
### Fragment Shorthand
Use `<>` for fragments. Only import and use `Fragment` when a `key` prop is required.
```tsx
// Good - no key needed
return (
<>
<Header />
<Content />
</>
);
// Good - key required
import { Fragment } from 'react';
return items.map((item) => (
<Fragment key={item.id}>
<ItemHeader item={item} />
<ItemContent item={item} />
</Fragment>
));
```
**Rationale:** `<>` is cleaner and more concise. `Fragment` is only needed for the `key` prop which `<>` doesn't support.
## Imports
### Single-Line Imports
Keep imports on a single line. If an import has too many items, split into multiple import statements.
```tsx
// Good
import { Button, Input, Select } from '@/components/ui';
import { Card, CardHeader, CardContent } from '@/components/ui/card';
// Good - split when too long
import { TableBody, TableCell, TableHead } from '@/components/ui/table';
import { TableHeader, TableRow } from '@/components/ui/table';
// Avoid - multiline imports
import { Button, Input, Select, Card } from '@/components/ui';
```
**Rationale:** Single-line imports are easier to scan and take less vertical space. Splitting by source module keeps related imports together.
## Utilities
### Use Existing Helpers
Prefer existing helper functions over inline implementations.
```tsx
// Good
import { formatCurrency } from 'helpers/formatters';
const display = formatCurrency(amount / 100, currency, 0);
// Avoid
const display = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: currency,
minimumFractionDigits: 0,
}).format(amount / 100);
```
**Rationale:** Consistent formatting across the app. Single place to modify behavior. Less code duplication and potential for bugs.
---
## Project Structure
```
src/
server.ts Bun.serve — public routes, /api/*, then the SPA
env-file.ts APP_ENV, layered env files, ambient environment last
servers/
create-router.ts createRouter() and HonoVariables
hono.ts the MOUNTS list, mounted under /api
errors.ts the one error envelope
serialize.ts every API serializer, in one module — its test walks the exports
api/<name>.ts one router per resource
databases/app_db/ Drizzle — see docs/DATABASE.md
apps/frontend/
index.html tracked template, holds __PUBLIC_URL__
index.gen.html generated, gitignored, what the server imports
index.tsx React entry
App.tsx the shell
globals.css Tailwind and the shadcn theme
components/ui/ shadcn primitives
lib/routes.ts hash routing
lib/api.ts the one fetch helper
features/<Feature>/ one folder per screen — see above
public/ served verbatim, globbed into routes at boot
scripts/gen-index.ts __PUBLIC_URL__ substitution
```
`@/*` resolves to `src/apps/frontend/*`, so `@/components/ui/button` and `@/lib/utils` work as shadcn
generates them.
## Assets and URLs
**Asset URLs in components must be relative — never start with `/`.**
```tsx
<img src="hero.webp" /> // ✅ resolves against <base>
<img src="/hero.webp" /> // ❌ escapes a path-prefix proxy
```
The app is served at `/` directly, and may be served under a prefix by a proxy. `<base href="/">` in
`index.html` is what a rewriting proxy changes, so relative URLs follow the app; a leading slash
bypasses it. `src/apps/frontend/asset-urls.test.ts` enforces this.
`fetch('/api/...')` is the exception — proxies that patch `window.fetch` rewrite absolute API paths.
Anything in `index.html` that a bundler would resolve on disk (a favicon `href`) must be an absolute
URL via `__PUBLIC_URL__` — Bun fails the build on a root-relative one.
## Configuration
`APP_ENV`, never `NODE_ENV` — see README. `NODE_ENV` is still set explicitly by the `dev` script
because React's conditional exports resolve against it: Bun emits `jsxDEV()` in development, and
React's production jsx-dev-runtime defines `jsxDEV` as `void 0`. The transform and the runtime must
agree.