Рефакторинг под FSD - пошаговое руководство с примерами

05 января 2026
Автор

Олег Марков

Введение

Рефакторинг под FSD (Feature Sliced Design) — это не про «переложить файлы по папкам». Это системная перестройка фронтенд‑проекта так, чтобы он стал проще для поддержки, масштабирования и внедрения новых фич.

Вы часто можете сталкиваться с ситуацией, когда:

  • любая новая фича затрагивает сразу десяток файлов;
  • общие компоненты размазаны по всему проекту;
  • одни и те же сущности описаны по‑разному;
  • непонятно, откуда можно импортировать что‑то, а откуда нельзя.

Feature Sliced Design как раз и отвечает на эти проблемы. Важно понимать: FSD — это не набор строго обязательных правил, а архитектурный подход. Его можно внедрять постепенно и адаптировать под ваш стек.

В статье я покажу вам, как подойти к рефакторингу существующего проекта с переходом к FSD:

  • с чего начать анализ текущей структуры;
  • как планировать переход и не «сломать все»;
  • какие слои и срезы FSD использовать;
  • как переносить компоненты, бизнес‑логику и API шаг за шагом;
  • какие правила импорта и зависимостей вам важно соблюдать;
  • какие типичные ошибки делают при рефакторинге под FSD.

Давайте разберемся по шагам.

Что такое FSD и зачем он нужен при рефакторинге

Кратко об идее Feature Sliced Design

Feature Sliced Design предлагает разбивать фронтенд‑приложение не по типу файлов (components, services, utils), а по функциональным срезам и слоям ответственности.

Три ключевые идеи, которые нам важны именно для рефакторинга:

  1. Функциональное разбиение по фичам
    Фича — это законченное с точки зрения пользователя действие или возможность (например, авторизация, оформление заказа, поиск и т.п.).

  2. Слои ответственности
    Каждый слой выполняет свою роль. На практике чаще всего используются:

    • app — инициализация приложения;
    • pages — страницы;
    • widgets — крупные, композиционные блоки на страницах;
    • features — пользовательские сценарии, завершенные с точки зрения UX;
    • entities — бизнес‑сущности (User, Product, Order);
    • shared — переиспользуемые примитивы, которые не знают про бизнес‑логку.
  3. Ограничения по зависимостям
    Верхние слои могут знать о нижних, но не наоборот. Например, features могут использовать entities и shared, но entities не должны знать о features.

Для рефакторинга это означает: нам предстоит:

  • выделить фичи и сущности;
  • разложить код по слоям;
  • ограничить зависимости и импорт.

Почему имеет смысл рефакторить существующий проект под FSD

Смотрите, какие типичные проблемы решает FSD:

  • Сложно локализовать изменения
    Исправляете авторизацию — меняется код в components, services, utils, иногда даже в pages. FSD говорит: все, что касается авторизации, должно быть в feature auth.

  • Отсутствие четких границ между модулями
    В проекте могут быть десятки взаимных импортов и «знаний» модуля А о деталях модуля Б. FSD предлагает провести границы: что внутри сущности, что внутри фичи, что является публичным API.

  • Трудности с переиспользованием
    Компоненты типа Button живут где‑то в components, но отдельные компоненты завязаны на конкретные данные (User, Order). После перевода на FSD вы лучше понимаете, что действительно можно переиспользовать, а что является частью конкретной фичи или сущности.

Поэтому при рефакторинге под FSD мы не «навешиваем новую структуру» на старый код, а раскладываем уже существующую функциональность по понятным слоям и срезам.

Планирование рефакторинга под FSD

Анализ текущего состояния проекта

Перед тем как что‑то переносить, полезно провести небольшой аудит. Я предлагаю вам ответить на несколько практических вопросов:

  1. Как сейчас структурированы файлы?

    • Есть ли папки components, services, store, hooks, utils?
    • Есть ли уже фиче‑подобные папки (auth, profile, cart)?
  2. Где живет бизнес‑логика?

    • В компонентах?
    • В сервисах?
    • В redux‑слайсах / zustand‑сторах?
  3. Как устроены импорты?

    • Есть ли общие алиасы вроде @/components, @/api?
    • Много ли «глубоких» импортов вглубь модулей?
  4. Что ломается чаще всего при изменениях?

    • Авторизация?
    • Список товаров?
    • Корзина?
    • Навигация?

Ответы помогут вам выделить основные фичи и сущности, с которых вы начнете.

Минимальный план по шагам

Давайте сформируем базовый пошаговый план рефакторинга:

  1. Настроить базовую FSD‑структуру папок.
  2. Выделить ключевые сущности (entities) и перенести их.
  3. Выделить основные фичи (features) поверх этих сущностей.
  4. Пересобрать страницы (pages) из widgets и features.
  5. Настроить правила импортов (ESLint + алиасы).
  6. Поэтапно переносить оставшиеся модули, избегая «большого взрыва».

Позже мы подробно разберем каждый шаг.

Подход к постепенному переходу

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

  • Добавить FSD‑структуру рядом с текущей.
  • Новые фичи писать уже по FSD.
  • Постепенно вынимать старые модули и переносить их в правильные срезы.

Это снижает риск поломок и дает команде время привыкнуть к новым правилам.

Базовая структура проекта под FSD

Пример структуры на уровне слоев

Смотрите, как может выглядеть корень src после первичного рефакторинга:

src/
  app/            // Инициализация приложения, провайдеры, роутер
  pages/          // Страницы
  widgets/        // Крупные композиционные блоки для страниц
  features/       // Пользовательские фичи (логин, поиск, фильтр и т.д.)
  entities/       // Бизнес-сущности (User, Product, Order)
  shared/         // Переиспользуемые примитивы

Комментарии по слоям:

  • app — точка входа, настройки роутинга, глобальные стили, провайдеры.
  • pages — формируют конкретные URL‑ы и собираются из widgets и features.
  • widgets — блоки типа «Header», «Sidebar», «ProductListSection».
  • features — логин, регистрация, фильтр товаров, смена языка, добавление в корзину и т.д.
  • entities — описание и логика вокруг доменных сущностей.
  • shared — кнопки, инпуты, вспомогательные функции, хелперы, UI‑кит.

Пример внутренней структуры shared

src/shared/
  ui/
    Button/
      ui.tsx      // Компонент кнопки
      styles.css  // Стили
      index.ts    // Публичный API кнопки
    Input/
      ui.tsx
      index.ts
  lib/
    formatDate/
      lib.ts      // Функция форматирования даты
      index.ts
  config/
    apiConfig/
      config.ts   // Общие настройки API
      index.ts

Комментарии:

  • в shared/ui — только примитивные и чистые компоненты без привязки к доменной области;
  • в shared/lib — функции без бизнес‑логики;
  • в shared/config — настройки, которые могут понадобиться на разных слоях.

Пример внутренней структуры entities

src/entities/
  user/
    model/
      types.ts       // Типы User
      selectors.ts   // Селекторы для User
      hooks.ts       // Хуки, связанные с User
    ui/
      UserAvatar/
        ui.tsx       // Компонент аватара пользователя
        index.ts
    api/
      userApi.ts     // Запросы, связанные с пользователем
    index.ts         // Публичный API сущности User

Как видите, здесь уже появляется доменная логика: типы пользователя, его API, специфичные для него компоненты.

Пример структуры features

src/features/
  authByEmail/
    model/
      slice.ts     // Redux slice или store
      selectors.ts
      hooks.ts
    ui/
      LoginForm/
        ui.tsx
        index.ts
    lib/
      validators.ts // Валидация формы логина
    index.ts        // Публичный API фичи

Фича authByEmail отвечает за конкретный пользовательский сценарий — авторизация по email и паролю. Внутри нее вы держите состояние, валидацию, API‑вызовы, UI.

Переход от «старой» структуры к FSD

Теперь давайте разберем, как перевести типичный «слоеный» проект (components, services, store, utils) к FSD.

Шаг 1. Создаем базовые директории FSD

На этом шаге вы просто добавляете новые папки, не трогая существующий код:

src/
  app/
  pages/
  widgets/
  features/
  entities/
  shared/
  components/   // Старый мир
  services/
  store/
  utils/

Пара советов:

  • Для shared можно сразу перенести туда базовые Button, Input, Modal из components, если они действительно универсальные.
  • Остальные старые модули пока не трогаем.

Шаг 2. Выделяем и переносим сущности

Смотрите, я покажу вам типичный набор сущностей в интернет‑магазине:

  • User
  • Product
  • Cart
  • Order

Давайте, для примера, перенесем User из старой структуры.

Предположим, сейчас у вас есть:

// src/store/userSlice.ts
// Здесь мы храним состояние пользователя
// (это условный пример)
export const userSlice = {/* ... */}

// src/services/userService.ts
// Здесь запросы к API, связанные с пользователем
export const fetchUser = async () => {/* ... */}

// src/components/UserAvatar.tsx
// Компонент аватара пользователя
export const UserAvatar = () => {/* ... */}

Теперь вы увидите, как это выглядит после переноса в entities/user:

// src/entities/user/model/slice.ts
// Здесь мы переносим состояние пользователя в слой сущности
export const userSlice = {/* ... */}

// src/entities/user/api/userApi.ts
// Запросы к API для работы с пользователем
export const fetchUser = async () => {
  // Логика вызова API
}

// src/entities/user/ui/UserAvatar/ui.tsx
// Компонент аватара пользователя
export const UserAvatar = () => {
  // Здесь используется сущность User (например, имя и фото)
  return (
    <div>
      {/* отображаем данные пользователя */}
    </div>
  )
}

// src/entities/user/index.ts
// Публичный API сущности User
export { UserAvatar } from "./ui/UserAvatar"
export { fetchUser } from "./api/userApi"
export { userSlice } from "./model/slice"

Теперь все, что касается пользователя как сущности, лежит в одном месте — внутри entities/user.

Когда вы в следующей фиче захотите использовать пользователя, вы импортируете сущность так:

// Импорт из публичного API сущности User
import { UserAvatar, fetchUser } from "@/entities/user"

Шаг 3. Выделяем фичи и переносим бизнес‑логику из компонентов

Частая проблема — в компонентах хранятся:

  • запросы к API;
  • сложные хендлеры;
  • стейт с бизнес‑логикой.

FSD предлагает вынести сценарий взаимодействия пользователя в отдельную фичу.

Давайте разберемся на примере логина.

Было: «толстый» компонент LoginForm

// src/components/LoginForm.tsx
// Компонент одновременно содержит UI, состояние и логику авторизации

const LoginForm = () => {
  const [email, setEmail] = useState("")  // Локальное состояние
  const [password, setPassword] = useState("")

  // Запрос к API прямо из компонента
  const handleSubmit = async () => {
    // Здесь вызывается API авторизации
    // и обрабатывается ответ
  }

  return (
    <form onSubmit={handleSubmit}>
      {/* разметка формы логина */}
    </form>
  )
}

Становится: фича authByEmail

Теперь давайте посмотрим, как это разложить в FSD.

// src/features/authByEmail/model/hooks.ts
// Здесь мы переносим бизнес-логику авторизации в фичу
import { useState } from "react"

export const useAuthByEmail = () => {
  // Локальное состояние, относящееся к процессу логина
  const [email, setEmail] = useState("")
  const [password, setPassword] = useState("")

  const handleSubmit = async () => {
    // Здесь можно вызывать API сущности User
    // например, loginUser(email, password)
    // и обрабатывать результат авторизации
  }

  // Возвращаем то, что нужно для UI
  return {
    email,
    setEmail,
    password,
    setPassword,
    handleSubmit,
  }
}
// src/features/authByEmail/ui/LoginForm/ui.tsx
// Задача компонента — отрендерить UI и связаться с моделью фичи
import { useAuthByEmail } from "../../model/hooks"

export const LoginForm = () => {
  const { email, setEmail, password, setPassword, handleSubmit } =
    useAuthByEmail()

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault()
        handleSubmit()
      }}
    >
      {/* поля ввода и кнопка */}
    </form>
  )
}
// src/features/authByEmail/index.ts
// Публичный API фичи авторизации по email
export { LoginForm } from "./ui/LoginForm"

Важно: бизнес‑логика сценария остается в features/authByEmail/model, а компонент LoginForm становится тоньше.

Шаг 4. Пересобираем страницы через widgets и features

Раньше у вас могла быть страница, которая напрямую собирается из общих компонентов:

// src/pages/LoginPage.tsx
// Страница логина, собранная напрямую из компонента LoginForm
import { LoginForm } from "@/components/LoginForm"

export const LoginPage = () => {
  return (
    <div>
      <LoginForm />
    </div>
  )
}

После рефакторинга вы строите страницу из фичи:

// src/pages/login/ui/LoginPage.tsx
// Страница логина, использующая фичу authByEmail
import { LoginForm } from "@/features/authByEmail"

export const LoginPage = () => {
  return (
    <div>
      <LoginForm />
    </div>
  )
}

// src/pages/login/index.ts
// Публичный API страницы логина
export { LoginPage } from "./ui/LoginPage"

Если на странице появляется сложная композиция (например, несколько фич + список товаров + фильтры), удобнее вынести часть структуры в widgets.

// src/widgets/AuthPanel/ui/AuthPanel.tsx
// Виджет панели авторизации, который может использоваться на разных страницах
import { LoginForm } from "@/features/authByEmail"

export const AuthPanel = () => {
  return (
    <section>
      <h1>Вход в систему</h1>
      <LoginForm />
    </section>
  )
}

// src/widgets/AuthPanel/index.ts
export { AuthPanel } from "./ui/AuthPanel"

И далее на странице:

// src/pages/login/ui/LoginPage.tsx
import { AuthPanel } from "@/widgets/AuthPanel"

export const LoginPage = () => {
  return <AuthPanel />
}

Правила импортов и зависимостей при рефакторинге

Базовое правило направленности зависимостей

Логика простая:

  • верхние слои (app, pages, widgets) могут использовать нижние (features, entities, shared);
  • наоборот — нельзя.

Смотрите таблицу разрешенных направлений:

  • app → может импортировать pages, widgets, features, entities, shared;
  • pages → может импортировать widgets, features, entities, shared;
  • widgets → может импортировать features, entities, shared;
  • features → может импортировать entities, shared;
  • entities → может импортировать shared;
  • shared → не должен импортировать ничего из доменных слоев.

Если вы замечаете, что, например, shared пытается импортировать что‑то из entities — это сигнал, что компонент не должен находиться в shared. Возможно, его место в entities или features.

Публичный API модулей

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

Например, вместо:

// Плохой пример - импорт во внутренности сущности
import { UserAvatar } from "@/entities/user/ui/UserAvatar/ui"

используйте:

// Хороший пример - импорт из публичного API сущности User
import { UserAvatar } from "@/entities/user"

Для этого:

  • в каждой фиче/сущности/виджете/странице создайте index.ts;
  • экспортируйте только то, что нужно снаружи.
// src/entities/user/index.ts
// Здесь мы явно определяем, что считается публичным API сущности User
export { UserAvatar } from "./ui/UserAvatar"
export { fetchUser } from "./api/userApi"
export type { User } from "./model/types"

Так вы можете в любой момент изменить внутреннюю структуру entities/user, не ломая импорты во всем проекте.

Настройка алиасов импортов

Чтобы было удобно рефакторить и не использовать относительные пути вида ../../../, стоит настроить алиасы в tsconfig.json или webpack.config.js.

Пример для TypeScript:

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

Теперь вы можете писать:

import { LoginPage } from "@pages/login"
import { AuthPanel } from "@widgets/AuthPanel"
import { LoginForm } from "@features/authByEmail"
import { UserAvatar } from "@entities/user"
import { Button } from "@shared/ui/Button"

Это облегчает дальнейший рефакторинг и анализ зависимостей.

Ограничение импортов через ESLint

Чтобы команда не нарушала архитектуру случайно, можно настроить ESLint‑правила.

Идея простая:

  • запрещаем импорт из «внутренностей» модулей по шаблону @features/**/model/** и т.п.;
  • разрешаем только импорты из корневых index.ts каждого модуля.

Можно использовать:

  • eslint-plugin-boundaries;
  • кастомные no-restricted-imports.

Пример фрагмента для no-restricted-imports:

// Фрагмент настройки ESLint
module.exports = {
  rules: {
    "no-restricted-imports": [
      "error",
      {
        patterns: [
          // Запрещаем импорт из глубоких путей внутри фич
          "@features/*/*/*",
          // Запрещаем импорт из глубоких путей внутри сущностей
          "@entities/*/*/*"
        ]
      }
    ]
  }
}

// В реальном проекте правила стоит настроить точнее под вашу структуру.

Организация состояния и побочных эффектов при рефакторинге под FSD

Где теперь хранить глобальное состояние

Если вы используете Redux, Zustand или другой глобальный стор, после перехода на FSD имеет смысл:

  • держать слайсы и стора сущностей в entities/**/model;
  • хранить состояние фич в features/**/model;
  • связывать все это на слое app (например, при конфигурации Redux store).

Пример связки Redux store в app:

// src/app/store.ts
// Здесь мы собираем корневой Redux store из сущностей и фич
import { configureStore } from "@reduxjs/toolkit"
import { userSlice } from "@entities/user"
import { authSlice } from "@features/authByEmail"

export const store = configureStore({
  reducer: {
    user: userSlice.reducer, // редьюсер сущности User
    auth: authSlice.reducer, // редьюсер фичи авторизации
  },
})

// Так app знает о entities и features, но не наоборот.

Куда девать сайд‑эффекты (запросы, таймеры)

Чтобы уменьшить связность и упростить тестирование:

  • запросы, связанные с конкретной сущностью (например, загрузка пользователя, списка продуктов), размещаем в entities/**/api;
  • запросы, связанные со сценарием (авторизация, смена пароля), размещаем в features/**/model или features/**/api.

Например, для фичи авторизации:

// src/features/authByEmail/api/authApi.ts
// Здесь мы описываем API-методы, связанные с авторизацией
export const loginByEmail = async (email: string, password: string) => {
  // HTTP-запрос к серверу
  // Возвращаем результат логина
}
// src/features/authByEmail/model/hooks.ts
import { loginByEmail } from "../api/authApi"

export const useAuthByEmail = () => {
  // Локальный стейт

  const handleSubmit = async () => {
    // Здесь вызывается loginByEmail
    // и обрабатываются результат и ошибки
  }

  return {
    // ...
  }
}

Это позволяет:

  • изолировать сайд‑эффекты;
  • использовать переиспользуемые сущностные запросы в разных фичах.

Работа с UI при рефакторинге под FSD

Как понять, что отправить в shared/ui, а что сделать частью фичи

Постарайтесь задать себе вопросы:

  1. Можно ли использовать этот компонент с другой сущностью или фичей без изменений?
  2. Знает ли компонент о доменных типах (User, Product, Order)?
  3. Содержит ли компонент бизнес‑логику или только отображение?

Если:

  • компонент отображает чистый UI без доменных типов (например, Button, Input, Modal) — его место в shared/ui;
  • компонент завязан на User — его место в entities/user/ui;
  • компонент реализует сценарий (например, LoginForm) — это features/authByEmail/ui.

Давайте посмотрим на два похожих компонента.

Пример: базовый Button в shared

// src/shared/ui/Button/ui.tsx
// Примитивная кнопка без доменной логики
type ButtonProps = {
  children: React.ReactNode
  onClick?: () => void
  disabled?: boolean
}

export const Button = ({ children, onClick, disabled }: ButtonProps) => {
  return (
    <button type="button" onClick={onClick} disabled={disabled}>
      {children}
    </button>
  )
}

Этот компонент ничего не знает о пользователях, товарах или корзине — он спокойно живет в shared/ui.

Пример: кнопка «Добавить в корзину» как фича

// src/features/addToCart/ui/AddToCartButton/ui.tsx
// Кнопка, которая реализует сценарий добавления товара в корзину
import { Button } from "@shared/ui/Button"
import { useAddToCart } from "../../model/hooks"

type AddToCartButtonProps = {
  productId: string
}

export const AddToCartButton = ({ productId }: AddToCartButtonProps) => {
  const { handleAddToCart, loading } = useAddToCart(productId)

  return (
    <Button onClick={handleAddToCart} disabled={loading}>
      Добавить в корзину
    </Button>
  )
}

А здесь уже явно есть бизнес‑смысл и сценарий — это фича addToCart, а не shared.

Рефакторинг «толстых» страниц

Иногда страницы изначально содержат много логики, которая:

  • дергает API;
  • управляет состоянием;
  • отрисовывает список сущностей.

В FSD логика «что именно на этой странице происходит» постепенно переезжает в:

  • widgets — композиция блоков;
  • features — сценарии;
  • entities — работа с конкретными данными.

Пример упрощенного процесса:

  1. Выносите всю бизнес‑логику (например, фильтрацию списка, сортировку) в фичу или сущности.
  2. Упрощаете страницу до «набора виджетов и фич с пропсами».
  3. Если возникает повторяемая комбинация (например, «страница списка товаров с фильтрами»), оформляете это как widget.

Типичные ошибки при рефакторинге под FSD

Ошибка 1. Перенос всей старой структуры внутрь FSD как есть

Иногда разработчики делают так:

src/features/
  auth/
    components/
    services/
    utils/

То есть «упаковывают» старую структуру в папку features. Формально папки называются по‑новому, но суть архитектуры не меняется.

Лучше стремиться к логике:

src/features/authByEmail/
  model/   // состояние, бизнес-логика
  ui/      // компоненты, привязанные к фиче
  api/     // API-запросы фичи (если есть)
  lib/     // хелперы фичи

Ошибка 2. Захламление shared бизнес-компонентами

Сюда часто попадают:

  • UserCard (хотя это сущность user);
  • ProductCard (сущность product);
  • LoginForm (фича authByEmail).

Если компонент знает про доменные типы или бизнес‑сценарии — его место точно не в shared.

Ошибка 3. Отсутствие публичных API и импорт «во внутренности»

Когда каждый импортирует то, что ему нужно, напрямую из глубины /model или /ui, мы быстро приходим к ситуации, когда:

  • модуль меняется — и ломается полпроекта;
  • нельзя легко «скрыть» внутренности модуля.

Поэтому старайтесь с самого начала:

  • создавать index.ts в каждой фиче/сущности/виджете;
  • импортировать только из этих index.ts.

Ошибка 4. Попытка «переписать все за раз»

Полный рефакторинг всего проекта под FSD одним коммитом — почти гарантированный источник багов. Лучше действовать итеративно:

  1. Ввести FSD структуру.
  2. Новые фичи сразу делать по FSD.
  3. Постепенно переносить старые модули, начиная с самых проблемных областей.

Пошаговый пример рефакторинга маленькой фичи

Теперь давайте посмотрим на более цельный пример, чтобы вы увидели весь процесс.

Представим простую фичу — «избранные товары». Сейчас она реализована так:

// src/components/FavoriteButton.tsx
// Компонент избранного, в котором вперемешку хранится логика и UI
import { useState } from "react"

type FavoriteButtonProps = {
  productId: string
}

export const FavoriteButton = ({ productId }: FavoriteButtonProps) => {
  const [isFavorite, setIsFavorite] = useState(false)

  const toggleFavorite = async () => {
    // Запрос к API, который добавляет/убирает товар из избранного
    setIsFavorite((prev) => !prev)
  }

  return (
    <button onClick={toggleFavorite}>
      {isFavorite ? "Убрать из избранного" : "В избранное"}
    </button>
  )
}

Шаг 1. Создаем сущность Product (если еще нет)

// src/entities/product/model/types.ts
// Тип товара
export type Product = {
  id: string
  title: string
  price: number
}
// src/entities/product/index.ts
// Публичный API сущности Product
export type { Product } from "./model/types"

Шаг 2. Создаем фичу toggleFavorite

// src/features/toggleFavorite/model/hooks.ts
// Здесь мы выносим бизнес-логику избранного в фичу
import { useState } from "react"

export const useToggleFavorite = (productId: string) => {
  const [isFavorite, setIsFavorite] = useState(false)

  const toggleFavorite = async () => {
    // Здесь должен быть вызов API для смены статуса избранного
    // после успешного ответа меняем состояние
    setIsFavorite((prev) => !prev)
  }

  return {
    isFavorite,
    toggleFavorite,
  }
}
// src/features/toggleFavorite/ui/FavoriteButton/ui.tsx
// UI-кнопка, использующая модель фичи
import { useToggleFavorite } from "../../model/hooks"

type FavoriteButtonProps = {
  productId: string
}

export const FavoriteButton = ({ productId }: FavoriteButtonProps) => {
  const { isFavorite, toggleFavorite } = useToggleFavorite(productId)

  return (
    <button onClick={toggleFavorite}>
      {isFavorite ? "Убрать из избранного" : "В избранное"}
    </button>
  )
}
// src/features/toggleFavorite/index.ts
// Публичный API фичи
export { FavoriteButton } from "./ui/FavoriteButton"
export { useToggleFavorite } from "./model/hooks"

Шаг 3. Используем фичу на странице или в виджете

// src/widgets/ProductCard/ui/ProductCard.tsx
// Виджет карточки товара
import { FavoriteButton } from "@features/toggleFavorite"
import type { Product } from "@entities/product"

type ProductCardProps = {
  product: Product
}

export const ProductCard = ({ product }: ProductCardProps) => {
  return (
    <article>
      <h2>{product.title}</h2>
      <p>{product.price} ₽</p>
      <FavoriteButton productId={product.id} />
    </article>
  )
}

Вы отделили:

  • бизнес‑сценарий «избранное» — в фичу toggleFavorite;
  • сущность Product — в entities/product;
  • UI карточки с кнопкой избранного — в widgets/ProductCard.

Заключение

Рефакторинг под FSD — это постепенная перестройка проекта вокруг:

  • функциональных срезов (фич);
  • доменных сущностей;
  • четко определенных слоев ответственности.

Ключевые практики, которые стоит удерживать в фокусе:

  • выделяйте сущности (entities) и фичи (features) из текущего кода;
  • раскладывайте код по слоям: app, pages, widgets, features, entities, shared;
  • соблюдайте направленность зависимостей — нижние слои не должны знать о верхних;
  • используйте публичные API модулей через index.ts;
  • не перегружайте shared бизнес‑логикой;
  • проводите рефакторинг итеративно — с постепенным переносом и параллельной разработкой новых фич уже по FSD.

Если вы будете следовать этим шагам и не пытаться перенести «все и сразу», проект станет структурированнее, а изменения — более локальными и предсказуемыми.


Частозадаваемые технические вопросы по теме

1. Как совместить FSD и монорепозиторий (Nx, Turborepo)?

Можно:

  • держать каждый крупный фронтенд‑приложение как отдельный пакет с собственной FSD‑структурой;
  • вынести shared на уровень монорепы как отдельный пакет (например, @acme/shared-ui), а внутри него тоже организовать структуру в стиле FSD, но чаще достаточно простого ui/lib/config.

Главное — не размывать границу между общими пакетами и конкретными FSD‑приложениями.

2. Что делать с legacy‑кодом, в котором много jQuery или старого React без хуков?

Практичный подход:

  • обернуть legacy‑модули адаптерами внутри нужного слоя (чаще всего features или widgets);
  • внутри адаптера инкапсулировать обращение к старому коду;
  • постепенно переписывать legacy‑логику уже в терминах FSD.

Снаружи ваша фича/виджет сразу будет выглядеть по‑новому, а реализацию можно менять по частям.

3. Как организовать тесты при FSD?

Рекомендуется:

  • располагать unit‑тесты рядом с кодом (ui.test.tsx, lib.test.ts) внутри соответствующей фичи/сущности;
  • для интеграционных/е2е‑тестов опираться на публичный API модулей и страницы (pages) — так тесты будут меньше зависеть от деталей реализации.

Важно, чтобы тесты не импортировали внутренние файлы, минуя index.ts, иначе архитектурные ограничения будут обходиться.

4. Как рефакторить глобальные стили и темы под FSD?

Чаще всего:

  • базовые темы и токены (цвета, отступы) уходят в shared/config или shared/theme;
  • глобальные стили и провайдеры темы находятся в app (например, ThemeProvider);
  • фичи и сущности опираются либо на токены из shared, либо на CSS‑переменные.

Старайтесь не тянуть конкретные стили фич в shared — там должны быть только общие вещи.

5. Как быть с кросс‑фичевыми зависимостями (например, фича авторизации и фича профиля)?

Если фича А зависит от фичи Б, это сигнал, что часть общей логики стоит вынести:

  • либо в сущность (entities/user);
  • либо в отдельную фичу более базового уровня (например, session).

После выноса каждая фича зависит не друг от друга, а от общей сущности/фичи‑базы, и это лучше соответствует идеям FSD.

Стрелочка влевоПошаговая миграция step-by-step - практическое руководство для разработчиковМиграция на FSD - практическое руководство по переходу к Feature Sliced DesignСтрелочка вправо

Все гайды по Fsd

Открыть базу знаний

Отправить комментарий