Використання Feature-Sliced Design(FSD) у Next.js

💡 Усі статті, обговорення, новини про Front-end — в одному місці. Приєднуйтесь до Front-end спільноти!

Вступ

Всім привіт! 👋 Я Frontend-розробник початківець. І мабуть багато хто з колег, особливо початківців, не раз стикався з проблемою: як організувати архітектуру проєкту? Що куди складати? Яку архітектуру взагалі використати?

Насправді чіткої відповіді немає. Немає й універсальної інструкції, бо все залежить від самого проєкту та обраного стеку. Але у своєму останньому pet-проєкті я захотів розібратися і попрацювати з популярним архітектурним підходом Feature-Sliced Design (FSD). Причому використати його разом із новим для мене фреймворком Next.js.

Прошу не судити строго — я сам початківець і завжди відкритий до зауважень та порад. Можливо, комусь цей досвід стане у пригоді і хтось знайде для себе щось цікаве та корисне. Мені здається, що вийшло доволі логічно та зручно, особливо якщо у майбутньому розширювати функціонал.

Коротко про DevQuizzer

Як приклад хочу розповісти про свій останній pet-проєкт — DevQuizzer, застосунок з квізами для розробників.

Функціонал:

  • форма для налаштування квізу (кількість питань, складність, категорії);
  • проходження квізу (одна/декілька відповідей, перехід між питаннями, повернення назад, відстеження прогресу);
  • сторінка результатів (можна переглянути відповіді з поясненнями, завантажити листівку з результатами та поділитися);
  • перемикач теми (світла/темна/автоматична).

Під час роботи найбільше користувався оригінальною документацією: overview. Є там і гайд для Next.js, але відверто — частина того, що описано, мені не знадобилася, а деяких речей якраз не вистачало. Тому я зробив трохи видозмінений варіант архітектури.

Повна структура застосунку

Щоб краще показати повну картину, ось структура застосунку:

app/                # Next.js app directory
  (home)/components # Головна сторінка та її блоки
  compose-quiz/     # Конфігуратор квізу
  quiz/             # Сторінка проходження
  results/          # Результати
  layout.tsx        # Загальний Layout
  globals.css       # Глобальні стилі

features/           # Бізнес-функціонал
  params/           # Конфігурація квізу
  ├── api/         # Запити на сервер для отримання категорій
  ├── store/       # Slice зі станом параметрів квізу
  └── ui/          # Компоненти форми для вибору налаштувань

  quiz/             # Логіка квізу та результатів
  ├── api/         # Запит на сервер для нового квізу
  ├── hooks/       # Кастомні хуки (localStorage, resetQuiz, навігація)
  ├── store/       # Slice для квізу та відповідей користувача
  └── ui/          # Усі UI-компоненти для проходження та результатів

  themeSwitcher/    # Перемикач теми
  ├── store/       # Slice для збереження теми
  └── ui/          # Компоненти перемикача

shared/             # Повторно використовувані частини
  providers/        # Провайдери (Tanstack Query, Material UI)
  schemas/          # Zod-схеми та типи
  ui/               # Header, Footer, Menu
  theme.ts          # Тема для Material UI

utils/              # Допоміжні функції (4 невеликі утиліти)

Чому саме FSD?

Мене привабило кілька моментів:

  • Масштабованість. З появою нових фіч легко зрозуміти, куди їх додавати.
  • Ізоляція. Кожна фіча має свій store, API, UI і не «лізе» у чужий простір.
  • Прозорість. Компоненти логічно згруповані, немає хаосу.
  • Гнучкість. Це не догма, а підхід, який можна підлаштувати.

1. Шари (Layers)

У моєму проєкті вийшло так:

- app/        # Next.js app directory (routing, pages, layout)
- features/   # Feature modules (quiz, params, themeSwitcher)
- shared/     # Shared UI, providers, schemas, types
- utils/      # Utility functions

Цього загалом було достатньо для такого невеликого застосунку, як мій. Одразу хочеться розібрати шари, які я не використав, або замінив:

  • pages/ — не потрібен у Next.js (усе в app/).
  • widgets/ — віджетів у мене просто немає.
  • entities/ — для мого випадку зайвий шар. Квіз як сутність добре лягає у features.
  • utils/ — я додав сам, бо звик, що різні утиліти лежать в окремому місці. Загалом їх можна винести у кожну конкретну фічу, але у мене застосунок невеликий, тож всі мої (4) утиліти можуть зберігатися в одному місці, і заплутатися буде важко.

2. Слайси (Slices)

Тепер можна перейти до більш детального розбору кожного шару окремо, а саме розібрати Slices:

1.Шар app

З app все доволі просто, бо тут, як я писав раніше, я зберігаю сторінки, тому структура виглядає наступним чином:

app
├── (home)/         # Головна сторінка
├── compose-quiz/   # Конфігуратор квізу
├── quiz/           # Сторінка проходження
├── results/        # Результати
├── layout.tsx      # Layout із провайдерами, header, footer
└── globals.css     # Глобальні стилі

В цілому все просто і лаконічно, особливо немає на чому зупинятися, тому можна більш детально розібрати структуру всередині сторінок. Кожна сторінка має наступну структуру:

page
├── page.tsx
├── page.module.scss

Всередині page.tsx у мене описана структура кожної сторінки. Наприклад:

imports...

export default function ComposeQuiz() {
   return (
     <Container className={styles.page} maxWidth="lg">
         <Typography variant="h2" component="h1">
            ✏️ Create your personal quiz
         </Typography>
         <ParamsForm />
      </Container>
   );
}

👉 Але (home) page має ще додаткову папку components. Мені здалося зручним: головна сторінка має більший розмір за інші, і я вирішив розбити її на окремі UI-блоки. Тому з’явилася така структура:

components
├── AboutAPI.tsx
├── APIFeature.tsx
├── HeroSection.tsx
├── HowItWorks.tsx
├── Step.tsx
├── TechStack.tsx
└── Styles.module.scss   # Загальні стилі для блоків

2.Шар features

Тут усе виглядає доволі просто, але не настільки очевидно, як у app/.

features
├── params         # Конфігурація квізу
├── quiz           # Основний функціонал
└── themeSwitcher  # Перемикач теми

Оскільки структура слайсів тут трохи складніша ніж у попередньому шарі, то думаю краще зупинитися на цьому детальніше у наступному розділі. Тут можу додати, що початково планував поділити quiz та results на різні слайси, але виникли труднощі: обидві фічі мали спільний store. Імпортувати його з одного в інший виглядало заплутано, а ще це порушувало принципи FSD та SOLID (Single Responsibility, ізольованість). Тому я вирішив залишити їх в одному slice. Чи це правильне рішення — не впевнений, буду радий вашим порадам у коментарях.

3.Шар shared

Також доволі цікавий шар, бо кожен буде зберігати там щось своє, залежно від застосунку та технологій. У мене вийшло так:

shared
├── providers   # провайдери для Tanstack Query і Material UI
├── schemas     # Zod-схеми та типи
├── ui          # Header, Footer, Menu
└── theme.ts    # Тема для Material UI

Я відмовився від окремої папки types, бо всі основні типи виводжу із Zod-схем. А якщо треба додати 2–3 дрібні типи, то тримаю їх прямо у файлі.

3. Сегменти (Segments)

Останній концепт, що варто розібрати у FSD, — це сегменти. Тобто ми ділимо кожен slice, або краще сказати кожну фічу (мені так більше подобається 😏). Найкращим прикладом для дослідження буде шар features, який ми у попередньому розділі залишили на потім. Тож маємо всього 3 фічі, і структура у них наступна:

features
├── params
│   ├── api
│   ├── store
│   └── ui
├── quiz
│   ├── api
│   ├── hooks
│   ├── store
│   └── ui
└── themeSwitcher
    ├── store
    └── ui

Як можна помітити, структура трохи відрізняється у різних фічах, але це більше залежить від складності та функцій окремої фічі.

  1. params Тут ми маємо наступний функціонал:
  • форма для запису параметрів квізу;
  • запит на сервер можливих категорій квізу;
  • маленький store, де ми зберігаємо обрані параметри.
  1. quiz Ця фіча найскладніша у застосунку, і тут структура трохи важча, але некритично. Вона виконує наступні функції:
  • робить запит на сервер для нового квізу;
  • записує та перевіряє відповіді користувача;
  • виводить результати;
  • дозволяє навігацію між питаннями;
  • працює з localStorage;
  • забезпечує можливість перезапуску вікторини.
  1. themeSwitcher Тут функціонал простіший:
  • зберігає обрану користувачем тему у store;
  • надає UI-компонент для перемикання теми.

Найкраще розглядати структуру фічі на прикладі найскладнішої з них — quiz:

  • api — описання всіх запитів до сервера;
  • hooks — тут описані кастомні хуки для роботи цієї фічі (це можуть бути допоміжні хуки по роботі з localStorage, легші хуки на кшталт resetQuiz, якщо треба обʼєднати декілька дій, ну і інше, думаю можна на цьому не зупинятися);
  • store — для кожної фічі правильніше використовувати окремий store, тут у store я зберігаю нормалізовано сам квіз та відповіді користувача;
  • ui — тут зберігаються всі UI-компоненти відповідної фічі окремо по папках, як зазвичай.

Можу додати по ui, що раніше я організовував компоненти з глибоким вкладенням, тобто:

ParentComponent/
├── ParentComponent.tsx
├── ParentComponent.module.scss
├── Child1/
│   ├── Child1.tsx
│   └── Child1.module.scss
└── Child2/
    └── ...

Але в даному випадку вирішив відмовитися від вкладень і просто всі компоненти зберігаю поруч в одній папці, навіть child, виглядає так:

ParentComponent/
├── ParentComponent.tsx
├── ParentComponent.module.scss
├── Child1.tsx
├── Child2.tsx

Кожний обере варіант для себе, особисто мені більше подобається перший, виглядає більш структуровано, але і коли вкладених компонентів стає більше, то вже важче орієнтуватися по такій структурі, бо глибина може бути понад 5 папок.

Висновок

Сподіваюся, ця стаття стане комусь корисною. Це мій особистий досвід і моє бачення можливого застосування FSD разом із Next.js. Це моя перша обʼємна публікація, тому буду радий будь-яким порадам.

Я чудово розумію, що у кожного розробника може бути свій підхід, свої уподобання та навіть власні «лайфхаки» при організації архітектури. У когось проєкти більші, у когось менші, і те, що добре спрацювало у мене, може не підійти в іншому випадку. Але головне — мати логіку та послідовність, щоб проєкт розвивався без хаосу.

Цей досвід показав мені, що навіть невеликий pet-проєкт може стати чудовим майданчиком для практики серйозних архітектурних підходів. І якщо ви тільки починаєте знайомство з FSD, то раджу спробувати застосувати його хоча б у маленькому проєкті — це допомагає зрозуміти сильні та слабкі сторони підходу.

Переваги мого підходу:

  • структура виглядає логічно та зрозуміло;
  • легко масштабувати й розширювати;
  • менше шансів отримати «спагеті-код».

А як ви організовуєте архітектуру у своїх проєктах? Поділіться в коментарях. 🙌

Посилання

👍ПодобаєтьсяСподобалось5
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Крута стаття, проте є декілька не зрозумілих моментів

Чому б не розмістити utils в shared/lib, здається що їм там було б саме місце, з рахунок чого стратегія шарів б не порушувалося

Щодо entities, хочу підсвітити те, що features не може містити в собі декларацію сутності by-design. В твоєму випадку мені здається, що варто було б рухатися саме з сутністю та фічами під неї. Про імпорти з features в shared вже відписали

Проте загалом — гідна реалізація та цікава стаття

Дякую за оцінку та корисні поради, обовʼязково візьму до увагу та спробую виправити всі протиріччя

Утиліти лежать у src, але фактично мають доступ і до shared, і до features. Це схоже на використання any у TypeScript: формально шари створені, проте існує директорія, яка виходить за межі ієрархії. Додатково, у shared трапляються імпорти з features, що знову порушує принципи імпортів з шарів. Отже: з cohesion ми впоралися, а з coupling — програли.

Додатково варто б подивитися на інтерфейси слайсів, тому що глибокі імпорти руйнують ту магію, за якої ми знаємо публічний інтерфейс і можемо редагувати що завгодно всередині.

Можна поставити eslint правила, щоб налаштувати все вищезазначене. P.S. в цілому, приємна архітектура.

FSD наче і нічого, але ж розробили цю архітектуру москалі, нам в Україні треба свій варіант feature-based підходу.

Доречі я нічого поганого не бачу в глибокій вкладеності, для пошуку компонента просто тицни по ньому в React devtools, а потім шукай по назві в коді. Чим вкладеність так лякає?

Мені загалом також більше імпонує варіант із вкладеністю. Дійсно, так виглядає більш структуровано. Але зустрічав і протилежні думки, що такий підхід тільки більше заплутує(тому і вирішив спробувати попрацювати без вкладеності, щоб зрозуміти, що мені більше до вподоби).

Стосовно москалів, тоді можливо краще розцінювати це просто як мою інтерпретацію feature-based без привʼязки саме до них))

Тримати під контролем владенність краще через те, що:
1) Добре, коли в проєкті легко орієнтуватися через IDE, розуміючи, з чого складається фіча, які компоненти охоплює і т.п. Девтулзи — ок, але не завжди захочеться в них лізти;
2) Зростає ментальна складність. В одному слайсі починаються імпорти по типу «.../.../../../utils/...», розробник не розуміє всієї картини відразу.

Підписатись на коментарі