Пошук уроків, статей та іншого контенту
Організуйте файли за функціональними можливостями, розділяючи features, спільний UI, сторінки та інфраструктурний код.
Feature-based структура організовує код не за типом файлу, а за функціональною можливістю застосунку.
Замість структури:
src/
components/
hooks/
services/
pages/
utils/використовують структуру, у якій пов’язані компоненти, хуки та логіка однієї можливості зберігаються разом:
src/
app/
pages/
features/
shared/
infrastructure/Наприклад, функція авторизації може містити форму, валідацію, запит до API та локальний стан в одному каталозі features/auth.
Це полегшує:
пошук коду;
зміну або видалення функціональності;
розділення відповідальностей;
повторне використання спільних компонентів;
контроль залежностей між частинами застосунку.
src/
app/
App.jsx
routes.jsx
providers/
AppProviders.jsx
pages/
HomePage/
HomePage.jsx
ProfilePage/
ProfilePage.jsx
features/
auth/
api/
login.js
components/
LoginForm.jsx
hooks/
useAuth.js
model/
authReducer.js
index.js
todos/
api/
todosApi.js
components/
TodoForm.jsx
TodoList.jsx
hooks/
useTodos.js
index.js
shared/
ui/
Button/
Button.jsx
Input/
Input.jsx
lib/
formatDate.js
config/
constants.js
infrastructure/
api/
client.js
storage/
localStorage.js
main.jsxКожен рівень має власну відповідальність.
appКаталог app містить код, який збирає застосунок:
кореневий компонент;
маршрутизацію;
глобальні провайдери;
підключення глобальних стилів;
конфігурацію застосунку.
Тут не повинна зберігатися логіка конкретної функціональної можливості.
pagespages містить компоненти сторінок. Сторінка зазвичай компонує кілька функцій і передає їм необхідні параметри.
Сторінка може знати про features, але окрема функція не повинна залежати від конкретної сторінки.
featuresfeatures — основна частина feature-based підходу.
Окрема feature — це завершена функціональна можливість, яку користувач може виконати або побачити:
авторизація;
створення завдання;
фільтрація списку;
додавання товару до кошика;
зміна налаштувань профілю.
Feature може містити:
компоненти;
хуки;
запити до API;
локальний стан;
валідацію;
допоміжні функції, потрібні лише цій feature.
sharedshared містить код, який не належить конкретній функції та може використовуватися в різних частинах застосунку.
Наприклад:
кнопка;
поле введення;
модальне вікно;
форматування дати;
універсальний хук;
константа загального призначення.
До shared не слід переносити будь-який код лише для того, щоб зробити структуру «чистішою». Якщо компонент використовується тільки в features/auth, він має залишатися в features/auth.
infrastructureinfrastructure містить технічні деталі взаємодії із зовнішнім середовищем:
HTTP-клієнт;
роботу з localStorage;
інтеграцію із зовнішніми сервісами;
налаштування доступу до API.
Feature використовує інфраструктурний код, але не повинна дублювати його реалізацію.
Розглянемо feature для керування завданнями.
features/
todos/
api/
todosApi.js
components/
TodoForm.jsx
TodoList.jsx
hooks/
useTodos.js
index.js// src/infrastructure/api/client.js
const API_URL = "https://jsonplaceholder.typicode.com";
export async function apiRequest(path, options = {}) {
const response = await fetch(`${API_URL}${path}`, {
headers: {
"Content-Type": "application/json",
...options.headers,
},
...options,
});
if (!response.ok) {
throw new Error(`Помилка API: ${response.status}`);
}
return response.json();
}// src/features/todos/api/todosApi.js
import { apiRequest } from "../../../infrastructure/api/client";
export function getTodos() {
return apiRequest("/todos?_limit=5");
}
export function createTodo(title) {
return apiRequest("/todos", {
method: "POST",
body: JSON.stringify({
title,
completed: false,
userId: 1,
}),
});
}Файл todosApi.js знає, які саме запити потрібні feature завдань. Загальна реалізація HTTP-запиту залишається в infrastructure.
// src/features/todos/hooks/useTodos.js
import { useCallback, useEffect, useState } from "react";
import { createTodo, getTodos } from "../api/todosApi";
export function useTodos() {
const [todos, setTodos] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const loadTodos = useCallback(async () => {
try {
setIsLoading(true);
setError(null);
const data = await getTodos();
setTodos(data);
} catch (requestError) {
setError(requestError);
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
loadTodos();
}, [loadTodos]);
async function addTodo(title) {
const newTodo = await createTodo(title);
setTodos((currentTodos) => [newTodo, ...currentTodos]);
}
return {
todos,
isLoading,
error,
addTodo,
};
}// src/features/todos/components/TodoForm.jsx
import { useState } from "react";
export function TodoForm({ onSubmit }) {
const [title, setTitle] = useState("");
async function handleSubmit(event) {
event.preventDefault();
const normalizedTitle = title.trim();
if (!normalizedTitle) {
return;
}
await onSubmit(normalizedTitle);
setTitle("");
}
return (
<form onSubmit={handleSubmit}>
<label>
Нове завдання
<input
value={title}
onChange={(event) => setTitle(event.target.value)}
placeholder="Наприклад, прочитати документацію"
/>
</label>
<button type="submit">Додати</button>
</form>
);
}// src/features/todos/components/TodoList.jsx
export function TodoList({ todos }) {
if (todos.length === 0) {
return <p>Завдань поки немає.</p>;
}
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.title}
</li>
))}
</ul>
);
}Файл index.js визначає, що дозволено імпортувати з feature назовні:
// src/features/todos/index.js
export { TodoForm } from "./components/TodoForm";
export { TodoList } from "./components/TodoList";
export { useTodos } from "./hooks/useTodos";Тепер сторінка імпортує feature через її публічний API:
import { TodoForm, TodoList, useTodos } from "../features/todos";Замість цього не варто використовувати глибокі імпорти:
// Небажано
import { TodoList } from "../features/todos/components/TodoList";Публічний API приховує внутрішню структуру feature. Якщо каталог components пізніше перейменують, імпорти сторінок не доведеться змінювати.
Сторінка відповідає за композицію:
// src/pages/HomePage/HomePage.jsx
import { TodoForm, TodoList, useTodos } from "../../features/todos";
export function HomePage() {
const { todos, isLoading, error, addTodo } = useTodos();
if (isLoading) {
return <p>Завантаження...</p>;
}
if (error) {
return <p>Не вдалося завантажити завдання.</p>;
}
return (
<main>
<h1>Мої завдання</h1>
<TodoForm onSubmit={addTodo} />
<TodoList todos={todos} />
</main>
);
}Кореневий компонент підключає сторінку:
// src/app/App.jsx
import { HomePage } from "../pages/HomePage/HomePage";
export function App() {
return <HomePage />;
}Такий приклад можна запустити у звичайному React-проєкті, створеному за допомогою Vite. Компоненти використовують лише стандартні API React і браузерний fetch.
Для підтримки зрозумілої архітектури корисно домовитися про напрямок залежностей:
app → pages → features → shared
↓
infrastructureНа практиці це означає:
app може використовувати сторінки;
pages може використовувати features і shared;
features може використовувати shared та infrastructure;
shared не повинна залежати від конкретної feature;
одна feature не повинна напряму використовувати внутрішні файли іншої feature.
Якщо дві features використовують однакову логіку, спочатку варто перевірити, чи справді це спільна абстракція. Не кожен подібний фрагмент потрібно негайно переносити в shared.
Поставте такі запитання:
Чи описує цей код дію або можливість користувача?
Чи потрібен він лише в одному функціональному контексті?
Чи можна видалити цю функцію разом із її компонентами, хуками та API?
Чи має цей код власний стан або правила предметної області?
Якщо відповіді переважно позитивні, код, імовірно, належить до features.
Наприклад:
features/
auth/
components/
LoginForm.jsx
hooks/
useAuth.js
api/
login.jsА універсальна кнопка належить до shared:
shared/
ui/
Button/
Button.jsxНе кожен компонент вимагає окремої папки в features.
Не варто створювати feature для:
простого текстового компонента;
універсальної кнопки;
невеликого компонента, який використовується лише на одній сторінці;
технічного допоміжного коду без окремої користувацької можливості.
Feature має бути достатньо великою, щоб мати власну функціональну межу. Надмірне подрібнення структури створює більше навігації, але не додає ясності.
components/
hooks/
services/
utils/У великому проєкті пошук усіх файлів, пов’язаних з однією можливістю, стає складним. Feature-based структура групує їх за призначенням.
shared на загальний складЯкщо в shared потрапляють компоненти з назвами UserProfileCard, OrderStatusBadge і CheckoutForm, це може означати, що вони належать конкретним features.
У shared мають залишатися справді загальні елементи.
// Небажано
import { authReducer } from "../auth/model/authReducer";Краще експортувати необхідний API через features/auth/index.js і використовувати його:
import { useAuth } from "../auth";Компонент не повинен містити всю реалізацію HTTP-запиту, форматування відповіді та керування помилками одночасно. Розділення на API, хук і UI спрощує тестування та заміну окремих частин.
Не потрібно переносити код у shared після першого повторення. Спочатку переконайтеся, що його поведінка та відповідальність справді однакові в усіх місцях використання.
Feature-based структура групує код за функціональними можливостями.
features містить завершені користувацькі сценарії.
pages компонує features у сторінки.
shared містить повторно використовувані та незалежні від бізнес-логіки елементи.
infrastructure ізолює роботу із зовнішніми сервісами.
app відповідає за складання та запуск застосунку.
Публічний index.js допомагає приховати внутрішню структуру feature.
Чіткі межі між каталогами зменшують зв’язаність і спрощують розвиток React-проєкту.