Компоненты
Хороший компонент имеет одну основную причину для изменения. Если компонент одновременно грузит данные, хранит форму, рендерит таблицу, управляет модалкой и форматирует даты, его стоит разделить.
Практичный разрез:
Page- собирает экран, роутинг, крупные блоки;Feature- бизнес-сценарий: фильтр, форма, список, wizard;Entity- отображение доменной сущности;Shared UI- переиспользуемые кнопки, поля, модалки;lib- чистые функции, адаптеры, форматтеры.
function UsersPage() {
return (
<main>
<UsersToolbar />
<UsersTable />
</main>
);
}Состояние
Перед добавлением глобального store стоит определить тип состояния:
- local UI state - открыта ли модалка, выбранная вкладка, значение инпута;
- server state - данные, полученные с API, кеш, статус запроса;
- url state - фильтры, сортировка, пагинация, выбранная сущность;
- global client state - авторизация, тема, feature flags, межстраничный UI.
Server state лучше хранить в TanStack Query, RTK Query, Apollo или аналогичном инструменте. Фильтры и пагинацию часто лучше держать в URL, чтобы работали back/forward, шаринг ссылки и восстановление страницы.
const searchParams = new URLSearchParams(location.search);
const page = Number(searchParams.get('page') ?? 1);
const status = searchParams.get('status') ?? 'all';Эффекты
useEffect нужен для синхронизации с внешней системой: DOM API, подписки, timers, аналитика, сторонний SDK. Если код только вычисляет значение из props/state, нужен не effect, а обычное вычисление или useMemo.
const visibleUsers = users.filter((user) => user.status === selectedStatus);useEffect(() => {
const controller = new AbortController();
void fetchUsers({ signal: controller.signal });
return () => controller.abort();
}, []);Не дублировать derived state
Если значение можно вычислить из уже существующего состояния, не стоит хранить его отдельно.
const completedCount = todos.filter((todo) => todo.completed).length;Плохой сигнал - useEffect, который при каждом изменении одного state пересчитывает и записывает другой state.
Compound components
Для связанного UI можно использовать compound-подход: внешне API остаётся читаемым, а внутреннее состояние скрыто внутри компонента.
<Tabs defaultValue="profile">
<Tabs.List>
<Tabs.Trigger value="profile">Профиль</Tabs.Trigger>
<Tabs.Trigger value="billing">Оплата</Tabs.Trigger>
</Tabs.List>
<Tabs.Panel value="profile">...</Tabs.Panel>
<Tabs.Panel value="billing">...</Tabs.Panel>
</Tabs>Custom hooks
Хук должен инкапсулировать поведение, а не просто прятать две строки кода. Хорошие кандидаты: работа с URL-параметрами, подписки, autosave, keyboard shortcuts, медиа-запросы, синхронизация с localStorage.
function useBoolean(defaultValue = false) {
const [value, setValue] = useState(defaultValue);
return {
value,
setTrue: () => setValue(true),
setFalse: () => setValue(false),
toggle: () => setValue((current) => !current),
};
}Стабильные props
Не нужно механически оборачивать всё в useMemo и useCallback. Они полезны, когда:
- значение передаётся в memoized-компонент;
- зависимость используется в тяжёлом вычислении;
- ссылка важна для подписки или внешнего API;
- профилирование показало лишние перерендеры.
const columns = useMemo(() => createUserColumns({ onEdit }), [onEdit]);Формы
Для больших форм лучше разделять:
- schema - правила и типы;
- default values - начальное состояние;
- submit adapter - преобразование формы в API payload;
- UI components - поля и layout.
import { z } from 'zod';
export const userFormSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
role: z.enum(['admin', 'manager', 'viewer']),
});
export type UserFormValues = z.infer<typeof userFormSchema>;Ошибки
Ошибка запроса, ошибка валидации и неожиданный exception - разные случаи. Для них должны быть разные места обработки:
- validation error - рядом с полем;
- request error - рядом с действием или секцией;
- render error - Error Boundary;
- unknown error - fallback и логирование.
function toErrorMessage(error: unknown) {
if (error instanceof Error) return error.message;
return 'Произошла неизвестная ошибка';
}Тестируемость
Чем меньше бизнес-логики в JSX, тем проще тестировать. Форматирование, фильтрацию, сортировку, нормализацию и доступы лучше выносить в чистые функции.
function canEditUser(currentUser: User, targetUser: User) {
return currentUser.role === 'admin' && currentUser.id !== targetUser.id;
}