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

6.7 KiB

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:

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.

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.

// 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.

// 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.

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.

// ✅ 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.

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.

// 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.

// 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.

// 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.