Навигация

Паттерны компонентов и файловая структура в React

Паттерны проектирования в React — это проверенные подходы к организации компонентов, управлению состоянием и построению архитектуры приложения. Они сформировались в экосистеме как ответы на повторяющиеся задачи и определяют разницу между кодом, который «работает», и кодом, который легко расширять и поддерживать. В статье разберём ключевые паттерны компонентов и подходы к организации файлов в React / Next.js проектах.

💡 Есть Vue-версия этой статьи: Паттерны компонентов и файловая структура во Vue / Nuxt. Многие паттерны (feature-based структура, FSD, Container/Presentational) применимы к обоим фреймворкам, но реализуются разными механизмами.

Содержание

  1. Композиция компонентов
  2. Compound Components
  3. Render Props
  4. Custom Hooks как паттерн
  5. Container / Presentational
  6. State Reducer
  7. Control Props
  8. Props Collection
  9. Структура файлов: подходы
  10. Структура файлов в Next.js App Router
  11. Организация общего кода
  12. Лучшие практики
  13. Антипаттерны
  14. Ключевые тезисы для интервью
  15. Заключение
  16. Полезные ссылки

💡 Многие паттерны из этой статьи в 2026 году частично заменены хуками и серверными компонентами, но они по-прежнему встречаются в кодовых базах и полезны для понимания дизайна API.

Композиция компонентов

Композиция — главный паттерн React. Вместо наследования (как в ООП) React строит сложное поведение через вложение и передачу компонентов друг другу.

children — базовая композиция

function Card({ children }) {
  return (
    <div className="card">
      {children}
    </div>
  );
}

function App() {
  return (
    <Card>
      <h2>Заголовок</h2>
      <p>Содержимое карточки</p>
    </Card>
  );
}

Именованные слоты через пропсы

Когда нужно несколько «слотов» для разных частей UI:

function Dialog({ header, body, footer }) {
  return (
    <div className="dialog">
      <div className="dialog__header">{header}</div>
      <div className="dialog__body">{body}</div>
      <div className="dialog__footer">{footer}</div>
    </div>
  );
}

<Dialog
  header={<h2>Подтверждение</h2>}
  body={<p>Вы уверены?</p>}
  footer={<Button onClick={handleConfirm}>Да</Button>}
/>

Композиция через render-пропсы (см. ниже)

Композиция позволяет менять поведение компонента без его изменения — ключевой принцип гибкой архитектуры.


Compound Components

Compound Components (составные компоненты) — паттерн, при котором несколько компонентов работают вместе, разделяя неявное состояние через Context. Пользователь API собирает компонент из частей, получая максимальную гибкость.

Зачем нужен

Представьте <Select> с фиксированным API:

// ❌ Жёсткий API — нельзя кастомизировать internals
<Select
  items={items}
  renderItem={(item) => <span>{item.name}</span>}
  renderTrigger={(open) => <button>{open ? "Close" : "Open"}</button>}
/>

Compound-подход даёт полный контроль над структурой:

// ✅ Compound Components — полная гибкость
<Select items={items}>
  <Select.Trigger>
    <Select.Icon />
    <Select.Value placeholder="Выберите..." />
  </Select.Trigger>
  <Select.Content>
    <Select.Search />
    <Select.List>
      {(item) => (
        <Select.Item key={item.id} value={item.id}>
          <Avatar src={item.avatar} />
          <span>{item.name}</span>
        </Select.Item>
      )}
    </Select.List>
  </Select.Content>
</Select>

Реализация через Context

const SelectContext = createContext(null);

function Select({ children, items, value, onChange }) {
  const [isOpen, setIsOpen] = useState(false);

  const context = {
    items,
    value,
    onChange,
    isOpen,
    setIsOpen,
  };

  return (
    <SelectContext value={context}>
      <div className="select">
        {children}
      </div>
    </SelectContext>
  );
}

function Trigger({ children }) {
  const { isOpen, setIsOpen } = use(SelectContext);
  return (
    <button onClick={() => setIsOpen(!isOpen)}>
      {children}
    </button>
  );
}

function Content({ children }) {
  const { isOpen } = use(SelectContext);
  if (!isOpen) return null;
  return <div className="select__content">{children}</div>;
}

function Item({ value, children }) {
  const { onChange, setIsOpen } = use(SelectContext);
  return (
    <div
      className="select__item"
      onClick={() => {
        onChange(value);
        setIsOpen(false);
      }}
    >
      {children}
    </div>
  );
}

Select.Trigger = Trigger;
Select.Content = Content;
Select.Item = Item;

Когда использовать

  • Библиотечные компоненты с кастомизацией (Radix UI, Headless UI)
  • Компоненты, где количество вариантов конфигурации слишком велико для пропсов
  • Когда нужно дать пользователю контроль над структурой DOM

Когда НЕ использовать

  • Простые компоненты с 2–3 пропсами
  • Когда составные части не разделяют состояние

💡 Популярные библиотеки на Compound Components: Radix Primitives, Headless UI, Chakra UI, Ark UI. Все они предоставляют «headless» API — логику и доступность без стилей.


Render Props

Render Props — паттерн, при котором компонент принимает функцию, возвращающую React-элемент. Эта функция вызывается внутри компонента с данными, которые нужно передать наружу.

function MouseTracker({ render }) {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
    window.addEventListener("mousemove", handleMove);
    return () => window.removeEventListener("mousemove", handleMove);
  }, []);

  return render(position);
}

<MouseTracker
  render={({ x, y }) => <p>Cursor: {x}, {y}</p>}
/>

Render Props vs Custom Hooks

До появления хуков render props был основным способом переиспользования логики. В 2026 году custom hooks заменили render props в большинстве случаев:

// ❌ Render Props (устаревший подход для переиспользования логики)
<MouseTracker render={({ x, y }) => <p>{x}, {y}</p>} />

// ✅ Custom Hook (современный подход)
function useMousePosition() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  useEffect(() => {
    const handleMove = (e) => setPosition({ x: e.clientX, y: e.clientY });
    window.addEventListener("mousemove", handleMove);
    return () => window.removeEventListener("mousemove", handleMove);
  }, []);
  return position;
}

function Cursor() {
  const { x, y } = useMousePosition();
  return <p>Cursor: {x}, {y}</p>;
}

Когда render props всё ещё полезен

  • В библиотеках, где нужно инвертировать контроль над рендерингом
  • Когда логика тесно связана с конкретным местом в JSX
  • Для «inversion of control» в composition-паттернах
function DataTable({ data, columns, renderRow }) {
  return (
    <table>
      <thead>
        <tr>
          {columns.map((col) => <th key={col.key}>{col.title}</th>)}
        </tr>
      </thead>
      <tbody>
        {data.map((row, i) => (
          <tr key={i}>
            {renderRow(row)}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

Custom Hooks как паттерн

Пользовательские хуки — основной паттерн переиспользования логики в современном React. Хук инкапсулирует состояние и побочные эффекты, возвращая чистый API.

Паттерн: инкапсуляция DOM-логики

function useClickOutside(ref, handler) {
  useEffect(() => {
    const listener = (event) => {
      if (!ref.current || ref.current.contains(event.target)) return;
      handler(event);
    };
    document.addEventListener("mousedown", listener);
    document.addEventListener("touchstart", listener);
    return () => {
      document.removeEventListener("mousedown", listener);
      document.removeEventListener("touchstart", listener);
    };
  }, [ref, handler]);
}

function Dropdown() {
  const [isOpen, setIsOpen] = useState(false);
  const ref = useRef(null);
  useClickOutside(ref, () => setIsOpen(false));
  return <div ref={ref}>...</div>;
}

Паттерн: абстракция API-запросов

function useApi(url, options = {}) {
  const [state, setState] = useState({
    data: null,
    error: null,
    isLoading: true,
  });

  useEffect(() => {
    const controller = new AbortController();

    async function fetchData() {
      try {
        setState((s) => ({ ...s, isLoading: true }));
        const res = await fetch(url, { ...options, signal: controller.signal });
        if (!res.ok) throw new Error(res.statusText);
        const data = await res.json();
        setState({ data, error: null, isLoading: false });
      } catch (error) {
        if (error.name !== "AbortError") {
          setState({ data: null, error, isLoading: false });
        }
      }
    }

    fetchData();
    return () => controller.abort();
  }, [url]);

  return state;
}

Паттерн: композиция хуков

Хуки можно комбинировать, создавая более высокоуровневые абстракции:

function useAuth() {
  const [user, setUser] = useState(null);
  const { data, isLoading } = useApi("/api/me");

  useEffect(() => {
    if (data) setUser(data.user);
  }, [data]);

  const login = async (credentials) => {
    const res = await fetch("/api/login", {
      method: "POST",
      body: JSON.stringify(credentials),
    });
    const data = await res.json();
    setUser(data.user);
  };

  const logout = () => {
    setUser(null);
    fetch("/api/logout", { method: "POST" });
  };

  return { user, isLoading, login, logout };
}

Container / Presentational

Паттерн разделения компонентов на два типа:

  • Container (умный) — отвечает за данные, логику, побочные эффекты
  • Presentational (глупый) — отвечает только за отображение, получает всё через пропсы
// Presentational — ничего не знает об источнике данных
function UserList({ users, isLoading, onUserClick }) {
  if (isLoading) return <Spinner />;
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id} onClick={() => onUserClick(user)}>
          {user.name}
        </li>
      ))}
    </ul>
  );
}

// Container — загружает данные и передаёт в презентационный
function UserListContainer() {
  const { data: users, isLoading } = useApi("/api/users");
  const navigate = useNavigate();

  return (
    <UserList
      users={users ?? []}
      isLoading={isLoading}
      onUserClick={(user) => navigate(`/users/${user.id}`)}
    />
  );
}

Актуальность в 2026

С появлением серверных компонентов и хуков паттерн Container/Presentational трансформировался:

  • Серверные компоненты по умолчанию являются «контейнерами» — они загружают данные
  • Клиентские компоненты становятся «presentational» — они получают данные через пропсы
  • Хуки заменили необходимость в отдельных контейнерных компонентах для большинства случаев
// Современная интерпретация: Server Component = Container
async function UserListPage() {
  const users = await db.users.findMany();
  return <UserList users={users} />;
}

// Клиентский компонент = Presentational
"use client";
function UserList({ users }) {
  // только UI-логика
}

Паттерн по-прежнему полезен для понимания разделения ответственности, но явное создание пар «контейнер + презентация» теперь избыточно.


State Reducer

Паттерн State Reducer позволяет потребителю компонента контролировать, как обновляется внутреннее состояние компонента, не меняя его поведение по умолчанию.

function Toggle({ children, reducer: externalReducer }) {
  const [on, setOn] = useState(false);

  const defaultReducer = (state, action) => {
    switch (action.type) {
      case "toggle": return { on: !state.on };
      case "reset": return { on: false };
      default: return state;
    }
  };

  const reducer = externalReducer
    ? (state, action) => externalReducer(defaultReducer, state, action)
    : defaultReducer;

  const handleToggle = () => {
    setState((prev) => reducer(prev, { type: "toggle" }));
  };

  return children({ on, toggle: handleToggle });
}

// Использование: отключаем toggle при определённом условии
<Toggle
  reducer={(defaultReducer, state, action) => {
    if (state.on && action.type === "toggle") return state;
    return defaultReducer(state, action);
  }}
>
  {({ on, toggle }) => <button onClick={toggle}>{on ? "ON" : "OFF"}</button>}
</Toggle>

Этот паттерн часто встречается в Headless UI-библиотеках, где нужно дать пользователю контроль над поведением без переписывания всей логики.


Control Props

Паттерн Control Props позволяет компоненту работать в двух режимах:

  • Uncontrolled — компонент сам управляет своим состоянием
  • Controlled — состояние управляется извне через пропсы

Это тот же паттерн, что используется в <input value={val} onChange={...} /> vs <input defaultValue="x" />.

function Toggle({ on: controlledOn, onChange, defaultOn = false }) {
  const [internalOn, setInternalOn] = useState(defaultOn);

  const isControlled = controlledOn !== undefined;
  const on = isControlled ? controlledOn : internalOn;

  const handleToggle = () => {
    const newValue = !on;
    if (!isControlled) {
      setInternalOn(newValue);
    }
    onChange?.(newValue);
  };

  return <button onClick={handleToggle}>{on ? "ON" : "OFF"}</button>;
}

// Uncontrolled
<Toggle defaultOn={false} onChange={(val) => console.log(val)} />

// Controlled
const [isOn, setIsOn] = useState(false);
<Toggle on={isOn} onChange={setIsOn} />

Когда использовать

  • Библиотечные компоненты (формы, модалки, аккордеоны)
  • Когда нужно поддержать оба режима использования
  • Для совместимости с управляемыми и неуправляемыми формами

Props Collection (State Aggregator)

Паттерн Props Collection группирует связанные пропсы в объект, упрощая API:

// ❌ Много отдельных пропсов
<DataTable
  sortField="name"
  sortOrder="asc"
  onSortFieldChange={setSortField}
  onSortOrderChange={setSortOrder}
  filterText=""
  onFilterTextChange={setFilterText}
/>

// ✅ Группировка в объекты
<DataTable
  sort={{ field: "name", order: "asc", onChange: setSort }}
  filter={{ text: "", onChange: setFilterText }}
/>

Структура файлов: подходы

Организация файлов — одна из самых субъективных тем в React-разработке. Нет «правильного» подхода, но есть проверенные стратегии.

1. По типу (Type-based)

Группировка по техническому назначению файлов:

src/
├── components/
│   ├── Button.tsx
│   ├── Modal.tsx
│   └── Input.tsx
├── hooks/
│   ├── useAuth.ts
│   └── useFetch.ts
├── services/
│   └── api.ts
├── utils/
│   └── format.ts
├── pages/
│   ├── Home.tsx
│   └── Dashboard.tsx
└── types/
    └── index.ts

Плюсы: Простая навигация, понятна для маленьких проектов. Минусы: При росте проекта папки components/ раздуваются до сотен файлов. Связанные файлы разбросаны по разным директориям.

2. По фиче (Feature-based)

Группировка по бизнес-домену:

src/
├── features/
│   ├── auth/
│   │   ├── components/
│   │   │   ├── LoginForm.tsx
│   │   │   └── SignupForm.tsx
│   │   ├── hooks/
│   │   │   └── useAuth.ts
│   │   ├── api/
│   │   │   └── authApi.ts
│   │   ├── types.ts
│   │   └── index.ts
│   ├── products/
│   │   ├── components/
│   │   │   ├── ProductCard.tsx
│   │   │   └── ProductList.tsx
│   │   ├── hooks/
│   │   │   └── useProducts.ts
│   │   ├── api/
│   │   │   └── productsApi.ts
│   │   └── index.ts
│   └── cart/
│       ├── components/
│       ├── hooks/
│       │   └── useCart.ts
│       ├── store/
│       │   └── cartStore.ts
│       └── index.ts
├── shared/
│   ├── components/
│   │   ├── Button.tsx
│   │   └── Modal.tsx
│   ├── hooks/
│   │   └── useDebounce.ts
│   ├── utils/
│   │   └── format.ts
│   └── types/
│       └── api.ts
└── app/
    ├── layout.tsx
    ├── providers.tsx
    └── router.tsx

Плюсы: Всё, что относится к фиче, в одном месте. Легко удалить фичу целиком. Масштабируется. Минусы: Дублирование инфраструктуры (каждая фича имеет свою components/, hooks/).

3. По домену (Domain-driven / Screaming Architecture)

Структура «кричит» о бизнес-домене:

src/
├── domains/
│   ├── user/
│   │   ├── User.ts
│   │   ├── UserRepository.ts
│   │   └── UserService.ts
│   ├── order/
│   │   ├── Order.ts
│   │   ├── OrderRepository.ts
│   │   └── OrderService.ts
│   └── product/
├── infrastructure/
│   ├── http/
│   │   └── apiClient.ts
│   ├── storage/
│   │   └── localStorage.ts
│   └── auth/
│       └── authProvider.ts
├── application/
│   ├── useCases/
│   │   ├── loginUser.ts
│   │   └── createOrder.ts
│   └── queries/
│       └── getProducts.ts
└── presentation/
    ├── pages/
    ├── components/
    └── hooks/

Плюсы: Чёткое разделение ответственности. Бизнес-логика независима от фреймворка. Минусы: Избыточная сложность для небольших проектов. Требует зрелой команды.

4. Feature-Sliced Design (FSD)

Популярная в СНГ-сообществе методология:

src/
├── app/              # инициализация приложения, провайдеры, роутинг
├── processes/        # сквозные бизнес-процессы (авторизация, оплата)
├── pages/            # страницы приложения
├── widgets/          # самостоятельные блоки UI (Sidebar, Header)
├── features/         # части функциональности (LikeButton, AddToCart)
├── entities/         # бизнес-сущности (User, Product, Order)
└── shared/           # переиспользуемый код (UI-kit, lib, API-клиент)

Каждый слой может импортировать только из слоёв ниже. Нарушение границ — ошибка архитектуры.

Плюсы: Строгие правила, предсказуемая структура, хорошо для больших команд. Минусы: Много уровней абстракции, избыточно для проектов < 50 страниц.


Структура файлов в Next.js App Router

Next.js App Router навязывает структуру через файловую систему:

src/
├── app/
│   ├── layout.tsx              # корневой layout
│   ├── page.tsx                # главная страница
│   ├── loading.tsx             # глобальный loading
│   ├── error.tsx               # глобальный error
│   ├── not-found.tsx           # 404
│   │
│   ├── (auth)/                 # route group — не влияет на URL
│   │   ├── login/
│   │   │   └── page.tsx
│   │   ├── register/
│   │   │   └── page.tsx
│   │   └── layout.tsx          # layout для auth-группы
│   │
│   ├── (dashboard)/
│   │   ├── layout.tsx          # layout с сайдбаром
│   │   ├── overview/
│   │   │   └── page.tsx
│   │   ├── settings/
│   │   │   └── page.tsx
│   │   └── profile/
│   │       └── page.tsx
│   │
│   ├── api/                    # Route Handlers
│   │   ├── users/
│   │   │   └── route.ts
│   │   └── webhooks/
│   │       └── stripe/
│   │           └── route.ts
│   │
│   └── [...slug]/
│       └── page.tsx            # catch-all
│
├── components/
│   ├── ui/                     # переиспользуемые UI-компоненты
│   │   ├── button.tsx
│   │   ├── input.tsx
│   │   └── dialog.tsx
│   ├── features/               # компонентные части фич
│   │   ├── auth/
│   │   │   ├── login-form.tsx
│   │   │   └── signup-form.tsx
│   │   └── products/
│   │       ├── product-card.tsx
│   │       └── product-grid.tsx
│   └── layouts/                # layout-компоненты
│       ├── sidebar.tsx
│       └── header.tsx
│
├── lib/                        # утилиты и конфигурация
│   ├── api/                    # API-клиент, запросы
│   │   ├── client.ts
│   │   ├── users.ts
│   │   └── products.ts
│   ├── auth/
│   │   └── session.ts
│   ├── db/
│   │   └── index.ts
│   └── utils.ts
│
├── hooks/                      # переиспользуемые хуки
│   ├── use-debounce.ts
│   └── use-media-query.ts
│
├── stores/                     # глобальное состояние (Zustand и т.д.)
│   ├── auth-store.ts
│   └── cart-store.ts
│
├── types/                      # TypeScript-типы
│   ├── api.ts
│   ├── auth.ts
│   └── product.ts
│
└── config/                     # конфигурация
    ├── site.ts
    └── env.ts

Рекомендации для Next.js

  • Route Groups (group) — для логической группировки без влияния на URL
  • Parallel Routes @modal, @team — для одновременного рендеринга нескольких страниц в одном layout
  • Intercepting Routes (.)route — для модалок и overlay-навигации
  • Колocate Server Actions с компонентами, которые их используют, или вынесите в lib/actions/

Организация общего кода

UI-кит (shared/components/ui)

Базовые компоненты без бизнес-логики:

components/ui/
├── button.tsx
├── input.tsx
├── dialog.tsx
├── dropdown-menu.tsx
├── toast.tsx
└── index.ts

Эти компоненты:

  • Не знают о бизнес-домене
  • Не делают API-запросов
  • Не используют глобальное состояние
  • Принимают всё через пропсы

Хуки (shared/hooks)

Переиспользуемые хуки без привязки к фиче:

hooks/
├── use-debounce.ts
├── use-media-query.ts
├── use-click-outside.ts
├── use-local-storage.ts
└── use-intersection-observer.ts

Утилиты (shared/lib)

Чистые функции и конфигурация:

lib/
├── utils.ts          # format, parse, validate
├── constants.ts      # enum, конфиг
├── api/
│   ├── client.ts     # настроенный fetch/axios
│   └── endpoints.ts  # URL-ы API
└── validators/
    ├── auth.ts       # Zod-схемы
    └── product.ts

Лучшие практики

1. Баррел-файлы (index.ts) для публичных API

// features/auth/index.ts
export { LoginForm } from "./components/login-form";
export { SignupForm } from "./components/signup-form";
export { useAuth } from "./hooks/use-auth";
export type { User, AuthState } from "./types";

⚠️ Баррел-файлы могут ухудшить tree-shaking в бандлерах. Используйте их для публичных API модулей, но не для components/ui/ с десятками компонентов.

2. Colocation — держите связанное рядом

features/auth/
├── login-form.tsx
├── login-form.test.tsx
├── login-form.stories.tsx
├── use-login.ts
├── login.schema.ts
└── types.ts

Тесты, сторибуки, схемы валидации и типы живут рядом с компонентом, а не в отдельных __tests__/, __stories__/ директориях.

3. Абсолютные импорты через алиасы

// tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"],
      "@/components/*": ["./src/components/*"],
      "@/features/*": ["./src/features/*"],
      "@/lib/*": ["./src/lib/*"],
      "@/hooks/*": ["./src/hooks/*"]
    }
  }
}
// ✅ Читаемый импорт
import { Button } from "@/components/ui/button";
import { useAuth } from "@/features/auth";

// ❌ Относительный импорт через 4 уровня
import { Button } from "../../../../components/ui/button";

4. Ограничение глубины вложенности

Максимум 3 уровня вложенности директорий. Если глубже — признак, что фича слишком большая и её нужно разделить.

5. Единая точка входа для API

// lib/api/client.ts
const apiClient = {
  async get<T>(url: string): Promise<T> {
    const res = await fetch(url, { headers: await getAuthHeaders() });
    if (!res.ok) throw new ApiError(res);
    return res.json();
  },
  async post<T>(url: string, body: unknown): Promise<T> {
    const res = await fetch(url, {
      method: "POST",
      headers: { "Content-Type": "application/json", ...(await getAuthHeaders()) },
      body: JSON.stringify(body),
    });
    if (!res.ok) throw new ApiError(res);
    return res.json();
  },
};

Все запросы проходят через один клиент — легко добавить interceptors, логирование, обработку ошибок.


Антипаттерны

1. God Component

Один компонент, который делает всё:

// ❌ 500+ строк, вся бизнес-логика в одном месте
function Dashboard() {
  const [users, setUsers] = useState([]);
  const [products, setProducts] = useState([]);
  const [orders, setOrders] = useState([]);
  const [filters, setFilters] = useState({});
  const [sort, setSort] = useState({});
  const [page, setPage] = useState(1);
  const [modal, setModal] = useState(null);

  useEffect(() => { /* загрузка пользователей */ }, []);
  useEffect(() => { /* загрузка продуктов */ }, []);
  useEffect(() => { /* загрузка заказов */ }, []);

  const handleUserClick = () => { /* ... */ };
  const handleProductDelete = () => { /* ... */ };
  const handleOrderUpdate = () => { /* ... */ };
  const handleFilterChange = () => { /* ... */ };
  const handleSortChange = () => { /* ... */ };
  const handlePageChange = () => { /* ... */ };
  const handleModalOpen = () => { /* ... */ };
  const handleExport = () => { /* ... */ };

  return (
    <div>
      {/* 500 строк JSX */}
    </div>
  );
}

Разбивайте на компоненты, хуки и утилиты.

2. Случайная структура

src/
├── components/
│   ├── NewButton.tsx
│   ├── old_modal.tsx
│   ├── userCard.tsx
│   └── temp/
│       └── test.tsx
├── utils/
│   ├── helpers.ts
│   ├── helpers2.ts
│   └── old_helpers.ts
└── api/
    ├── api.ts
    └── newApi.ts

Нет конвенции именования, дублирование, временные файлы становятся постоянными.

3. Circular Dependencies

// user.ts
import { Order } from "./order";
export type User = { orders: Order[] };

// order.ts
import { User } from "./user";
export type Order = { user: User };

Используйте интерфейсы, type-only imports (import type) или рефакторите в общий файл типов.

4. Избыточная абстракция

// ❌ 5 файлов для одного компонента
// Button.types.ts
// Button.styles.ts
// Button.utils.ts
// Button.constants.ts
// Button.tsx

// ✅ Имеет смысл только если действительно 5+ вариантов и сложная логика
// Button.tsx — всё в одном файле

5. Import from implementation details

// ❌ Импорт из внутреннего пути фичи
import { LoginForm } from "@/features/auth/components/login-form";

// ✅ Импорт из публичного API
import { LoginForm } from "@/features/auth";

Ключевые тезисы для интервью

  • Композиция — главный паттерн React: сложное поведение строится через вложение компонентов, а не наследование. Compound Components реализуют это через Context, разделяя состояние между подкомпонентами и давая контроль над DOM-структурой (основа Radix UI, Headless UI).
  • Render Props почти полностью вытеснены custom hooks — основным способом переиспользования логики в современном React; render props остаются полезны для инверсии контроля над рендерингом.
  • Container/Presentational трансформировался в эпоху Server Components: серверные компоненты стали контейнерами (загрузка данных, логика), клиентские — презентационными (интерактивность, UI).
  • Control Props позволяют компоненту работать в двух режимах — controlled (внешнее состояние) и uncontrolled (внутреннее), как <input> с value/defaultValue.
  • Feature-based структура масштабируется лучше type-based: всё, что относится к фиче, живёт рядом. Feature-Sliced Design формализует это строгими правилами импортов между слоями (app → pages → widgets → features → entities → shared).
  • В Next.js App Router структура задаётся файловой системой: route groups, parallel routes и intercepting routes расширяют возможности маршрутизации без дополнительных конфигов.
  • Colocation (тесты, сторибуки, схемы рядом с компонентом) удобнее отдельных __tests__/. Баррел-файлы (index.ts) полезны для публичных API, но могут ломать tree-shaking в UI-китах с десятками компонентов.

Заключение

React-паттерны — это словарь, на котором разговаривают senior-разработчики: композиция вместо наследования, хуки вместо HOC, Server Components вместо ручных контейнеров. Структура файлов — не догма, а компромисс между простотой навигации и масштабируемостью. Для маленьких проектов достаточно type-based, для средних — feature-based, для больших — FSD или domain-driven подход. Главный критерий выбора: могу ли я быстро найти нужный код и легко ли удалить фичу целиком.

Полезные ссылки