Files
platform/CONVENTIONS.md
T
2026-02-16 19:34:35 +00:00

251 lines
6.7 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
### No useMemo or useCallback (React 19)
**NEVER** use `useMemo` or `useCallback`. React 19's compiler handles memoization automatically. Importing and using these hooks is strictly prohibited.
```tsx
// ✅ Good - just write the code naturally
const filteredItems = items.filter((item) => item.active);
const handleClick = () => doSomething();
const stats = computeStats(data);
// ❌ Never do this - remove all useMemo/useCallback
const filteredItems = useMemo(() => items.filter((item) => item.active), [items]);
const handleClick = useCallback(() => doSomething(), []);
```
**Rationale:** React 19's compiler optimizes re-renders automatically. Manual memoization adds complexity without benefit and can actually prevent optimizations. The compiler is smarter than manual memoization.
### 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.