first
This commit is contained in:
+250
@@ -0,0 +1,250 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user