Сайт использует сookies для хранения данных. Продолжая использовать сайт, вы даёте согласие на работу с этими файлами.

ОК
💻
Технологии
Опубликовано:
14.09.2026
Обновлено:
14.09.2026

Feature-Sliced Design (FSD) в React + TypeScript: полное руководство по архитектуре масштабируемых фронтенд-приложений

Тимофей Ищенко

Коротко: Разбираем Feature-Sliced Design (FSD) в React и TypeScript: слои, слайсы, сегменты, правила импортов, Public API, практический пример и частые ошибки.

Когда React-приложение перерастает рамки MVP, классическая структура каталогов начинает тормозить разработку. Папка components превращается в свалку сотен компонентов с неявными зависимостями, хуки в hooks связывают несвязанные части системы, а рефакторинг любого модуля грозит непредсказуемыми поломками в других частях интерфейса.

Feature-Sliced Design (FSD) — это архитектурная методология, которая решает проблему масштабирования фронтенда. Она заменяет группировку файлов «по техническому типу» на структурирование по бизнес-ценности и зонам ответственности с жестко регламентированными правилами зависимостей.


Что такое Feature-Sliced Design и какие проблемы он решает

Кризис классической структуры: почему папки components, hooks и services перестают работать

Большинство базовых руководств по React предлагают группировать файлы по их технической роли:

src/
├── components/
├── hooks/
├── services/
├── utils/
└── types/

На ранних стадиях проекта такая схема кажется удобной. Однако с ростом функциональности возникают системные проблемы:

  1. Размытая ответственность. Компонент кнопки корзины лежит в components/CartButton.tsx, логика корзины — в hooks/useCart.ts, типы — в types/cart.ts, а запросы — в services/cartApi.ts. Чтобы внести изменение в одну функциональность, разработчик вынужден открывать файлы из диаметрально противоположных директорий.
  2. Неконтролируемые циклические зависимости. Компоненты начинают импортировать друг друга по горизонтали, создавая эффект карточного домика: удаление старой промо-акции ломает оформление заказа.
  3. Высокий порог входа. Новому разработчику сложно понять границы предметной области проекта, глядя на список из сотен изолированных файлов.

Фундаментальные принципы FSD: High Cohesion, Low Coupling и контролируемые зависимости

FSD опирается на принципы модульности и предметно-ориентированного проектирования (DDD):

  • Высокая связность (High Cohesion): код, решающий одну бизнес-задачу (UI, запросы, состояние, хелперы), находится в одном месте.
  • Слабая связанность (Low Coupling): модули изолированы друг от друга и взаимодействуют только через явные интерфейсы (Public API).
  • Однонаправленный поток зависимостей: модули верхнего уровня могут обращаться к нижним, но обратные или горизонтальные импорты строго запрещены.

Анатомия FSD: слои (layers), слайсы (slices) и сегменты (segments)

Методология разделяет кодовую базу на три иерархических уровня:

Слой (Layer) ➔ Слайс (Slice) ➔ Сегмент (Segment)
src/
├── app/                  # Слой
├── pages/                # Слой
│   └── product-details/  # Слайс
│       ├── ui/           # Сегмент
│       ├── model/        # Сегмент
│       └── index.ts      # Public API слайса

Иерархия слоев сверху вниз

Слои строго стандартизированы. Их последовательность определяет допустимые направления импортов:

  1. app — инициализация приложения: роутер, провайдеры контекста (Redux, TanStack Query), глобальные стили и точки входа.
  2. pages — маршруты и законченные страницы. Слой отвечает за компоновку виджетов под конкретный URL.
  3. widgets — крупные самодостаточные блоки интерфейса (шапка сайта, интерактивный каталог, сайдбар профиля).
  4. features — сценарии взаимодействия пользователя с системой, несущие непосредственную бизнес-ценность (добавление в корзину, фильтрация каталога, переключение темы, авторизация).
  5. entities — бизнес-сущности предметной области (пользователь, товар, заказ, статья). Содержат UI-отображение сущности, базовое состояние и методы работы с API.
  6. shared — переиспользуемый инфраструктурный код, не привязанный к бизнес-логике приложения: UI-kit (кнопки, инпуты, модалки), HTTP-клиент, утилиты форматирования дат, базовые TypeScript-хелперы.

Слайсы: декомпозиция по бизнес-доменам

Слои pages, widgets, features и entities разбиваются на слайсы. Слайс — это изолированная смысловая единица (директория).

  • В слое entities: user, product, order.
  • В слое features: auth-by-email, add-to-cart, search-products.
  • В слое widgets: header, product-card-grid, cart-drawer.

Слои app и shared на слайсы не делятся. В shared модули группируются сразу по сегментам (shared/ui, shared/api, shared/lib).

Сегменты: техническое наполнение

Внутри каждого слайса код распределяется по функциональному назначению:

  • ui — React-компоненты, стили, анимации.
  • model — бизнес-логика: хранилища состояния (Zustand, Redux slices), хуки, селекторы.
  • api — сетевые запросы, DTO, эндпоинты, мапперы данных.
  • lib — вспомогательные функции и утилиты, специфичные исключительно для этого слайса.
  • config — локальные конфигурационные константы.

Золотое правило FSD: направление зависимостей и изоляция

Главная ценность FSD заключается в жестких ограничениях на импорты.

    ┌──────────────┐
    │     app      │
    └──────┬───────┘
           │
           ▼
    ┌──────────────┐
    │    pages     │
    └──────┬───────┘
           │
           ▼
    ┌──────────────┐
    │   widgets    │
    └──────┬───────┘
           │
           ▼
    ┌──────────────┐
    │   features   │
    └──────┬───────┘
           │
           ▼
    ┌──────────────┐
    │   entities   │
    └──────┬───────┘
           │
           ▼
    ┌──────────────┐
    │    shared    │
    └──────────────┘

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

  • Модуль на слое pages может импортировать код из widgets, features, entities, shared.
  • Модуль на слое features может импортировать только entities и shared.
  • Модуль на слое entities может использовать исключительно shared.
  • Слой shared не имеет зависимостей от других слоев приложения.

Запрет горизонтальных связей между слайсами

Слайсы внутри одного слоя не могут импортировать друг друга. Например, features/add-to-cart не имеет права напрямую импортировать features/auth-by-email.

Если двум фичам нужно общее действие, логика либо выносится на слой выше (в widgets или pages), либо декомпозируется вниз (в entities или shared).

Public API: фасад модуля через index.ts

Каждый слайс обязан иметь входной файл index.ts. Он выполняет роль фасада, явно декларируя, какие компоненты, хуки и типы доступны внешнему миру:

// features/add-to-cart/index.ts
export { AddToCartButton } from './ui/add-to-cart-button';
export { useAddToCart } from './model/use-add-to-cart';
export type { AddToCartPayload } from './model/types';

Внешние модули имеют право импортировать код только из корня слайса:

// Корректно:
import { AddToCartButton } from '@/features/add-to-cart';

// Нарушение Public API:
import { AddToCartButton } from '@/features/add-to-cart/ui/add-to-cart-button';

Прямой доступ к внутренностям слайса запрещен. Это позволяет проводить внутренний рефакторинг без риска сломать внешние вызовы.


Разбор ключевой дилеммы: в чем разница между entities, features и widgets

Наибольшие сложности при освоении FSD вызывает разграничение средних слоев.

┌─────────────────────────────────────────────────────────────┐
│                          WIDGETS                            │
│  Композиция данных и действий (Header, ProductCardWithBuy)  │
└──────────────────────────────┬──────────────────────────────┘
                               │
               ┌───────────────┴───────────────┐
               ▼                               ▼
┌─────────────────────────────┐ ┌─────────────────────────────┐
│          FEATURES           │ │          ENTITIES           │
│ Действия пользователя       │ │ Данные и их отображение     │
│ (AddToCart, ToggleFavorite) │ │ (ProductCard, UserAvatar)   │
└─────────────────────────────┘ └─────────────────────────────┘

entities — данные и сущности предметной области

Слой entities представляет бизнес-концепции продукта. Он содержит модель данных, сетевые вызовы для получения этих данных и компоненты их пассивного отображения.

Примеры:

  • Карточка товара (ProductCard) с названием, ценой и изображением.
  • Аватар и краткая информация о пользователе (UserProfileCard).
  • Хук useProduct(id) для загрузки данных товара.

Важное ограничение: entities не содержат кнопок, запускающих самостоятельные бизнес-сценарии (например, кнопку «Купить» или «Удалить»).

features — действия и сценарии пользователя

Слой features реализует действия, приносящие прямую пользу бизнесу и пользователю.

Примеры:

  • Кнопка «Добавить в корзину» с обработкой состояния загрузки и отправкой события в аналитику.
  • Форма авторизации.
  • Строка поиска с debounce и подсказками.

widgets — самостоятельные композиционные блоки

Виджеты объединяют entities и features в законченные UI-структуры.

Пример: Полноценная карточка товара в ленте каталога: берет компонент отображения товара из entities/product и внедряет в него кнопку покупки из features/add-to-cart и кнопку добавления в избранное из features/toggle-favorite.


Настройка и использование FSD в связке с React и TypeScript

Конфигурация путей: path aliases в tsconfig.json

Для чистых импортов настраиваются алиасы путей к каждому слою:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/app/*": ["src/app/*"],
      "@/pages/*": ["src/pages/*"],
      "@/widgets/*": ["src/widgets/*"],
      "@/features/*": ["src/features/*"],
      "@/entities/*": ["src/entities/*"],
      "@/shared/*": ["src/shared/*"]
    }
  }
}

В сборщике (например, Vite через vite-tsconfig-paths или кастомные алиасы resolve.alias) эти пути синхронизируются с конфигурацией TypeScript.

Строгая типизация Public API: изоляция внутренних типов и DTO

TypeScript помогает предотвратить утечку серверных типов (DTO) в бизнес-логику интерфейса:

// entities/product/api/types.ts — внутренний тип API
export interface ProductServerDto {
  id: string;
  product_name: string;
  price_cents: number;
}

// entities/product/model/types.ts — доменный тип приложения
export interface Product {
  id: string;
  name: string;
  price: number;
}

В Public API слайса entities/product/index.ts наружу экспортируется только доменный интерфейс Product. Преобразование ProductServerDto в Product инкапсулируется внутри сегмента api или model.

Автоматизация контроля архитектуры с помощью линтеров

Человеческий фактор неизбежно приводит к случайным нарушениям правил импорта. Для автоматического контроля используются специализированные инструменты:

  • eslint-plugin-boundaries — позволяет гибко настроить типы модулей и ограничения на импорты между ними.
  • @feature-sliced/eslint-config — готовый набор правил для FSD.

Пример конфигурации ограничений через ESLint:

// .eslintrc.js
module.exports = {
  rules: {
    'boundaries/element-types': [
      2,
      {
        default: 'disallow',
        rules: [
          { from: 'app', allow: ['pages', 'widgets', 'features', 'entities', 'shared'] },
          { from: 'pages', allow: ['widgets', 'features', 'entities', 'shared'] },
          { from: 'widgets', allow: ['features', 'entities', 'shared'] },
          { from: 'features', allow: ['entities', 'shared'] },
          { from: 'entities', allow: ['shared'] },
          { from: 'shared', allow: ['shared'] },
        ],
      },
    ],
  },
};

Практический пример: каталог и добавление товара в корзину

Рассмотрим реализацию взаимодействия слоев при добавлении товара в корзину.

1. Слой entities/product

Сущность отвечает только за отображение данных товара и предоставление слотов для действий:

// src/entities/product/ui/product-card.tsx
import { ReactNode } from 'react';
import type { Product } from '../model/types';

interface ProductCardProps {
  product: Product;
  actionSlot?: ReactNode;
}

export const ProductCard = ({ product, actionSlot }: ProductCardProps) => {
  return (
    <article className="rounded-lg border p-4 shadow-sm">
      <img src={product.imageUrl} alt={product.name} className="h-48 w-full object-cover" />
      <h3 className="mt-2 text-lg font-semibold">{product.name}</h3>
      <p className="text-gray-600">${product.price.toFixed(2)}</p>
      {actionSlot && <div className="mt-4">{actionSlot}</div>}
    </article>
  );
};
// src/entities/product/index.ts
export { ProductCard } from './ui/product-card';
export type { Product } from './model/types';

2. Слой features/add-to-cart

Фича содержит кнопку с логикой отправки действия в стейт или на сервер:

// src/features/add-to-cart/ui/add-to-cart-button.tsx
import { Button } from '@/shared/ui/button';
import { useCartStore } from '@/entities/cart';

interface AddToCartButtonProps {
  productId: string;
}

export const AddToCartButton = ({ productId }: AddToCartButtonProps) => {
  const addToCart = useCartStore((state) => state.addItem);

  const handleClick = () => {
    addToCart(productId);
  };

  return (
    <Button onClick={handleClick} variant="primary">
      В корзину
    </Button>
  );
};
// src/features/add-to-cart/index.ts
export { AddToCartButton } from './ui/add-to-cart-button';

3. Сборка widgets/product-card-view и страницы pages/catalog

Виджет выполняет композицию: объединяет entities/product и features/add-to-cart через механизм слотов (actionSlot):

// src/widgets/product-card-view/ui/product-card-view.tsx
import { ProductCard, type Product } from '@/entities/product';
import { AddToCartButton } from '@/features/add-to-cart';

interface ProductCardViewProps {
  product: Product;
}

export const ProductCardView = ({ product }: ProductCardViewProps) => {
  return (
    <ProductCard
      product={product}
      actionSlot={<AddToCartButton productId={product.id} />}
    />
  );
};

Страница pages/catalog собирает сетку виджетов:

// src/pages/catalog/ui/catalog-page.tsx
import { ProductCardView } from '@/widgets/product-card-view';
import { useCatalogProducts } from '../model/use-catalog-products';

export const CatalogPage = () => {
  const { products, isLoading } = useCatalogProducts();

  if (isLoading) return <div>Загрузка каталога...</div>;

  return (
    <main className="container mx-auto py-8">
      <h1 className="mb-6 text-2xl font-bold">Каталог товаров</h1>
      <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
        {products.map((product) => (
          <ProductCardView key={product.id} product={product} />
        ))}
      </div>
    </main>
  );
};

Благодаря такой структуре ProductCard остается независимым: его можно переиспользовать в админ-панели, подставив в actionSlot кнопку «Редактировать» вместо покупки.


Типичные ошибки при внедрении FSD и способы их избежать

1. Превращение shared в свалку кода

Частая ошибка начинающих команд — складывать в shared всё подряд: компоненты со сложной доменной логикой, контексты пользователей и глобальные запросы.

Решение: В shared может находиться только код, не знающий специфики предметной области. Если кнопка в shared содержит текст «Купить курс» или зависит от роли пользователя — это грубая ошибка декомпозиции.

2. Создание избыточных слоев (Overengineering)

Попытка сразу разбить простую форму из одного текстового поля на три отдельных слайса в entities, features и widgets.

Решение: Начинайте с простого. Если функциональность не переиспользуется и представляет собой локальный UI-блок, ее можно разместить непосредственно внутри слайса конкретной страницы в pages/.../ui. Выносить код в features или entities следует тогда, когда возникает потребность в композиции или повторном использовании.

3. Нарушение Public API и глубокие импорты

Импорт файлов в обход index.ts уничтожает инкапсуляцию и связывает модули намертво.

Решение: Настройте линтеры на запрет относительных импортов за пределы текущего слайса (../) и запрет прямых путей к сегментам чужих модулей.


Когда FSD необходим, а когда избыточен: вердикт архитектора

Feature-Sliced Design — мощный инструмент, но он не является универсальной серебряной пулей для каждого проекта.

Критерий FSD рекомендован FSD избыточен
Размер команды От 3–4 разработчиков, работающих над кодовой базой параллельно 1 разработчик
Жизненный цикл Долгоживущие enterprise-приложения, B2B/B2C платформы Быстрые MVP, лендинги, промо-сайты со сроком жизни до полугода
Сложность бизнес-логики Множество ролей, сущностей, интеграций и сценариев Базовый CRUD с парой экранов
Требования к рефакторингу Высокая частота смены бизнес-требований Стабильный, неизменный функционал

FSD раскрывается в средних и крупных проектах, где критически важно снизить стоимость внесения изменений, упростить онбординг новых инженеров и свести к минимуму возникновение циклических зависимостей.


Часто задаваемые вопросы (FAQ)

1. Обязательно ли использовать все слои FSD в проекте?

Нет. Слои widgets или features могут отсутствовать в простых проектах или на ранних стадиях разработки. Если в вашем приложении нет сложной композиции, страница pages может напрямую агрегировать entities и shared. Главное требование — сохранять относительный порядок зависимостей для тех слоев, которые вы используете.

2. Как FSD работает с Next.js App Router?

Next.js App Router использует файловую маршрутизацию внутри папки app/. В FSD-архитектуре системная директория app/ фреймворка используется исключительно как тонкий слой роутинга: файлы page.tsx и layout.tsx просто импортируют и рендерят компоненты из слоя pages вашей FSD-структуры (например, src/pages/home).

3. Что делать, если двум слайсам в features нужно взаимодействовать?

Горизонтальные импорты между фичами запрещены. Есть три архитектурных решения:

  1. Вынести общее состояние и типы на уровень ниже — в слой entities.
  2. Реализовать координацию на уровень выше — внутри widgets или pages, передавая обработчики через пропсы или композицию.
  3. Использовать шину событий или глобальные действия через стейт-менеджер, не связывая UI-компоненты напрямую.

4. Где хранить глобальный стейт (Redux, Zustand) и инстанс API-клиента?

Базовый инстанс API-клиента (например, сконфигурированный axios или fetch) размещается в shared/api. Конфигурация глобального стора (создание Redux Store, корневые провайдеры) находится в слое app. При этом слайсы состояния конкретных сущностей лежат внутри соответствующих слайсов entities/*/model или features/*/model и подключаются к корневому стору на уровне app.

5. В чем разница между shared/ui и entities?

Компоненты в shared/ui — это абстрактные строительные блоки дизайн-системы (Input, Button, Modal, Card). Они ничего не знают о пользователях, товарах, ценах или заказах. Компоненты в entities привязаны к конкретной предметной области: они принимают доменные объекты (например, Product) и отображают специфичные для бизнеса атрибуты.

6. Считается ли слой processes устаревшим?

Да, в актуальной спецификации FSD слой processes признан устаревшим (deprecated). Ранее он использовался для сквозных многошаговых сценариев (например, сложных шагов оформления заказа). На практике такой функционал эффективнее и проще реализуется внутри композиционных виджетов или слайсов страниц.


Заключение

Feature-Sliced Design переносит акцент с технической группировки файлов на прозрачную доменную структуру приложения. Четкие правила изоляции модулей, явный Public API и однонаправленный поток зависимостей избавляют кодовую базу от запутанных связей, снижают техдолг и позволяют команде масштабировать React-приложение годами без необходимости регулярного переписывания с нуля.

Источники

Это авторская статья, основанная на личном опыте и субъективном взгляде автора. Заметили ошибку или битую ссылку? Сообщите нам: info@codesrc.ru - мы оперативно исправим. Спасибо, что помогаете делать блог лучше.
Следите за нами в соцсетях:

Читайте также