Олег Марков
Рефакторинг под FSD - пошаговое руководство с примерами
Введение
Рефакторинг под FSD (Feature Sliced Design) — это не про «переложить файлы по папкам». Это системная перестройка фронтенд‑проекта так, чтобы он стал проще для поддержки, масштабирования и внедрения новых фич.
Вы часто можете сталкиваться с ситуацией, когда:
- любая новая фича затрагивает сразу десяток файлов;
- общие компоненты размазаны по всему проекту;
- одни и те же сущности описаны по‑разному;
- непонятно, откуда можно импортировать что‑то, а откуда нельзя.
Feature Sliced Design как раз и отвечает на эти проблемы. Важно понимать: FSD — это не набор строго обязательных правил, а архитектурный подход. Его можно внедрять постепенно и адаптировать под ваш стек.
В статье я покажу вам, как подойти к рефакторингу существующего проекта с переходом к FSD:
- с чего начать анализ текущей структуры;
- как планировать переход и не «сломать все»;
- какие слои и срезы FSD использовать;
- как переносить компоненты, бизнес‑логику и API шаг за шагом;
- какие правила импорта и зависимостей вам важно соблюдать;
- какие типичные ошибки делают при рефакторинге под FSD.
Давайте разберемся по шагам.
Что такое FSD и зачем он нужен при рефакторинге
Кратко об идее Feature Sliced Design
Feature Sliced Design предлагает разбивать фронтенд‑приложение не по типу файлов (components, services, utils), а по функциональным срезам и слоям ответственности.
Три ключевые идеи, которые нам важны именно для рефакторинга:
Функциональное разбиение по фичам
Фича — это законченное с точки зрения пользователя действие или возможность (например, авторизация, оформление заказа, поиск и т.п.).Слои ответственности
Каждый слой выполняет свою роль. На практике чаще всего используются:app— инициализация приложения;pages— страницы;widgets— крупные, композиционные блоки на страницах;features— пользовательские сценарии, завершенные с точки зрения UX;entities— бизнес‑сущности (User, Product, Order);shared— переиспользуемые примитивы, которые не знают про бизнес‑логку.
Ограничения по зависимостям
Верхние слои могут знать о нижних, но не наоборот. Например,featuresмогут использоватьentitiesиshared, ноentitiesне должны знать оfeatures.
Для рефакторинга это означает: нам предстоит:
- выделить фичи и сущности;
- разложить код по слоям;
- ограничить зависимости и импорт.
Почему имеет смысл рефакторить существующий проект под FSD
Смотрите, какие типичные проблемы решает FSD:
Сложно локализовать изменения
Исправляете авторизацию — меняется код вcomponents,services,utils, иногда даже вpages. FSD говорит: все, что касается авторизации, должно быть в featureauth.Отсутствие четких границ между модулями
В проекте могут быть десятки взаимных импортов и «знаний» модуля А о деталях модуля Б. FSD предлагает провести границы: что внутри сущности, что внутри фичи, что является публичным API.Трудности с переиспользованием
Компоненты типаButtonживут где‑то вcomponents, но отдельные компоненты завязаны на конкретные данные (User,Order). После перевода на FSD вы лучше понимаете, что действительно можно переиспользовать, а что является частью конкретной фичи или сущности.
Поэтому при рефакторинге под FSD мы не «навешиваем новую структуру» на старый код, а раскладываем уже существующую функциональность по понятным слоям и срезам.
Планирование рефакторинга под FSD
Анализ текущего состояния проекта
Перед тем как что‑то переносить, полезно провести небольшой аудит. Я предлагаю вам ответить на несколько практических вопросов:
Как сейчас структурированы файлы?
- Есть ли папки
components,services,store,hooks,utils? - Есть ли уже фиче‑подобные папки (
auth,profile,cart)?
- Есть ли папки
Где живет бизнес‑логика?
- В компонентах?
- В сервисах?
- В redux‑слайсах / zustand‑сторах?
Как устроены импорты?
- Есть ли общие алиасы вроде
@/components,@/api? - Много ли «глубоких» импортов вглубь модулей?
- Есть ли общие алиасы вроде
Что ломается чаще всего при изменениях?
- Авторизация?
- Список товаров?
- Корзина?
- Навигация?
Ответы помогут вам выделить основные фичи и сущности, с которых вы начнете.
Минимальный план по шагам
Давайте сформируем базовый пошаговый план рефакторинга:
- Настроить базовую FSD‑структуру папок.
- Выделить ключевые сущности (
entities) и перенести их. - Выделить основные фичи (
features) поверх этих сущностей. - Пересобрать страницы (
pages) изwidgetsиfeatures. - Настроить правила импортов (ESLint + алиасы).
- Поэтапно переносить оставшиеся модули, избегая «большого взрыва».
Позже мы подробно разберем каждый шаг.
Подход к постепенному переходу
Обратите внимание: не нужно пытаться за один спринт перенести весь проект. Более продуктивный путь:
- Добавить 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. Выделяем и переносим сущности
Смотрите, я покажу вам типичный набор сущностей в интернет‑магазине:
UserProductCartOrder
Давайте, для примера, перенесем 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, а что сделать частью фичи
Постарайтесь задать себе вопросы:
- Можно ли использовать этот компонент с другой сущностью или фичей без изменений?
- Знает ли компонент о доменных типах (
User,Product,Order)? - Содержит ли компонент бизнес‑логику или только отображение?
Если:
- компонент отображает чистый 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— работа с конкретными данными.
Пример упрощенного процесса:
- Выносите всю бизнес‑логику (например, фильтрацию списка, сортировку) в фичу или сущности.
- Упрощаете страницу до «набора виджетов и фич с пропсами».
- Если возникает повторяемая комбинация (например, «страница списка товаров с фильтрами»), оформляете это как
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 одним коммитом — почти гарантированный источник багов. Лучше действовать итеративно:
- Ввести FSD структуру.
- Новые фичи сразу делать по FSD.
- Постепенно переносить старые модули, начиная с самых проблемных областей.
Пошаговый пример рефакторинга маленькой фичи
Теперь давайте посмотрим на более цельный пример, чтобы вы увидели весь процесс.
Представим простую фичу — «избранные товары». Сейчас она реализована так:
// 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.