Second pass. These three are the ones a new contributor reads first, and all three were teaching things that are not true here. src/databases/CLAUDE.md claimed "Three PostgreSQL databases", listed two, and there is exactly one. Its type examples were Screenshot / Experiment / Company / GanOauth — none of which have ever existed in this repo; it had been carried over from another project wholesale. Rewritten against the real schema, queries and types, and it now carries the two things that actually bite: push-not-migrations, and the rule that the schema is the source of truth for what the database may CONTAIN, not just its shape — with the sql.raw trap in check() written down, since getting it wrong breaks push for the whole schema. src/apps/CLAUDE.md had the same problem in its examples (useExperimentsList, ExperimentCard, a state/ directory layout that does not exist), listed a `useWebsockets` hook that is not there while omitting useChatWebSocket, usePanelChannel and useJobs, and closed with links to three app docs that have never existed. Examples now use real hooks, and it points at the navigation audit — a frontend doc that did not mention the one rule the platform CLAUDE.md calls authoritative was a real gap. CONVENTIONS.md said, in bold, that useMemo and useCallback are "strictly prohibited" because "React 19's compiler handles memoization automatically". Wrong twice: the React Compiler is an opt-in build plugin that is NOT installed here, so React 19 memoizes nothing on its own — and roughly 40 files use each hook regardless, including code added this week. Replaced with guidance that matches both reality and the actual tradeoff, and says plainly what it used to claim. A rule that is false and universally ignored makes every other rule in the file look optional. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
263 lines
7.3 KiB
Markdown
263 lines
7.3 KiB
Markdown
# Code Conventions
|
|
|
|
This document outlines the coding patterns and conventions used in this project.
|
|
|
|
## 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]);
|
|
```
|
|
|
|
**This section used to say "NEVER — React 19's compiler handles memoization automatically", which was
|
|
wrong twice.** The React Compiler is a separate, opt-in build plugin and it is **not installed here**
|
|
(there is no `babel-plugin-react-compiler` in `package.json`); React 19 on its own memoizes nothing. And
|
|
the codebase never followed the rule — some 40 files use each hook. A convention that is both false and
|
|
universally ignored is worse than none, because it makes every other rule in this file look optional.
|
|
|
|
### 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>
|
|
));
|
|
|
|
// Avoid - unnecessary Fragment import
|
|
import { Fragment } from 'react';
|
|
|
|
return (
|
|
<Fragment>
|
|
<Header />
|
|
<Content />
|
|
</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.
|