Як структурувати frontend у застосунках на Laravel, Inertia та React

Перекладено ШІ 0 Spatie 30 липня, 2026

За десять років розробки на Laravel ми вивели оптимальну структуру frontend-частини на базі Inertia та React. Дізнайтеся, як організувати компоненти й модулі для максимально ефективної роботи всієї команди.

Протягом останнього десятиліття архітектура наших PHP-застосунків на Laravel залишалася стабільною. Водночас frontend-стек постійно змінювався: від jQuery до React і Vue, а потім назад до React у поєднанні з Inertia та TypeScript.

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

Структура frontend-коду залежить від типу застосунку. Це може бути як величезний проект на Inertia з сотнями сторінок та спільними компонентами, так і складний Single Page App на кшталт клону Figma. У цьому дописі ми розберемо наш базовий підхід до структурування, який може адаптуватися під потреби проекту.

Для початку поглянемо на типову структуру Inertia React застосунку:

resources
├── css
│   └── app.css
└── js
    ├── common
    │   ├── button
    │   │   └── Button.tsx
    │   └── card
    │       ├── Card.tsx
    │       ├── CardHeader.tsx
    │       └── CardContent.tsx
    ├── modules
    │   ├── auth
    │   │   ├── Avatar.ts
    │   │   └── useCurrentUser.ts
    │   └── categories
    │       └── CategoryBadge.tsx
    ├── pages
    │   ├── layouts
    │   │   └── Layout.tsx
    │   ├── profile
    │   │   ├── layouts
    │   │   │   └── ProfileLayout.tsx
    │   │   └── ProfilePage.tsx
    │   ├── posts
    │   │   ├── components
    │   │   │   └── PublishStatus.tsx
    │   │   ├── helpers
    │   │   │   └── generateSlug.ts
    │   │   ├── CreatePostPage.tsx
    │   │   ├── EditPostPage.tsx
    │   │   └── PostsIndexPage.tsx
    │   └── DashboardPage.tsx
    ├── shadcn
    │   ├── dialog.tsx
    │   ├── input.tsx
    │   └── select.tsx
    └── app.tsx

Розглянемо деталі.

Структура JavaScript-застосунку

Ми використовуємо чотири основні директорії в resources/js:

  • common: Універсальні модулі та компоненти. Теоретично їх можна переносити між проектами, хоча стилі зазвичай залишаються специфічними для кожного сайту.
  • modules: Специфічний для проекту код. Це компоненти, що використовуються на багатьох сторінках, або логіка, яку варто винести за межі папки pages.
  • pages: Компоненти сторінок для Inertia. Вони можуть містити власний локальний код або допоміжні компоненти.
  • shadcn: Автоматично згенеровані компоненти з shadcn/ui.

Common та modules

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

Простий модуль у common, наприклад button або card, може містити лише кілька компонентів:

resources
└── js
    └── common
        ├── button
        │   └── Button.tsx
        └── card
            ├── CardContents.tsx
            ├── CardHeader.tsx
            └── Card.tsx

Складніший модуль, наприклад agenda, варто структурувати за типами файлів:

resources
└── js
    └── modules
        └── agenda
            ├── components
            │   ├── Agenda.tsx
            │   ├── ListView.tsx
            │   └── GridView.tsx
            ├── contexts
            │   └── AgendaContext.tsx
            ├── helpers
            │   └── parseDate.ts
            ├── hooks
            │   └── useAgenda.ts
            └── types.ts

Піддиректорії за типами

Для організації модуля зазвичай використовуються такі папки:

components
contexts
constants
helpers
hooks
stores

Типи ми зазвичай збираємо в одному файлі types.ts, але якщо він стає занадто великим, створюємо окрему папку types. Глобальні утиліти (наприклад, helpers або hooks) можуть розміщуватися безпосередньо в корені common.

resources
└── js
    └── common
        ├── helpers
        │   └── parseDateFromServer.ts
        └── hooks
            └── useIntersectionObserver.ts

Правила найменування (Casing)

Ми дотримуємося чітких стандартів:

  • Файли, що експортують компоненти або React-контексти — PascalCase.
  • Усі інші файли — camelCase.
  • Директорії — kebab-case (як назви npm-модулів).

Common проти modules

Якщо ви не впевнені, куди покласти код, запитайте себе: «Чи стосується це конкретної бізнес-логіки або фічі застосунку?». Якщо так — це modules.

Pages

Тут живуть компоненти сторінок, які викликає Inertia. Структура цієї папки зазвичай повторює структуру URL. Назви файлів мають суфікс Page.

Також ми маємо папку layouts для глобальних макетів. Якщо сторінку потрібно розбити на частини або додати специфічні helpers чи hooks, ми використовуємо ту саму структуру папок за типами, щоб у корені pages залишалися лише чисті компоненти сторінок.

resources
└── js
    └── pages
        ├── layouts
        │   └── Layout.tsx
        ├── profile
        │   ├── layouts
        │   │   └── ProfileLayout.tsx
        │   └── ProfilePage.tsx
        ├── posts
        │   ├── components
        │   │   └── PublishStatus.tsx
        │   ├── helpers
        │   │   └── generateSlug.ts
        │   ├── CreatePostPage.tsx
        │   ├── EditPostPage.tsx
        │   └── PostsIndexPage.tsx
        └── DashboardPage.tsx

Shadcn

Ми використовуємо shadcn/ui для швидкого старту проекту, щоб не витрачати тижні на створення власної бібліотеки компонентів. З часом ми переносимо та адаптуємо ці компоненти до папки common.

Ми уникаємо значних змін у самих компонентах shadcn, оскільки API, яке вони використовують разом із Radix, часто є занадто низькорівневим для повсякденної розробки.

export function FruitSelect() {
    return (
        <Select>
            <SelectTrigger>
                <SelectValue placeholder="Select a fruit" />
            </SelectTrigger>
            <SelectContent>
                <SelectGroup>
                    <SelectLabel>Fruits</SelectLabel>
                    <SelectItem value="apple">Apple</SelectItem>
                    <SelectItem value="banana">Banana</SelectItem>
                    <SelectItem value="blueberry">Blueberry</SelectItem>
                    <SelectItem value="grapes">Grapes</SelectItem>
                    <SelectItem value="pineapple">Pineapple</SelectItem>
                </SelectGroup>
            </SelectContent>
        </Select>
    )
}

Такий підхід зручний для створення UI-бібліотеки, але у прикладному коді ми часто створюємо простіші абстракції:

export function FruitSelect() {
    return (
        <Select
            placeholder="Select a fruit"
            options={[
                { value: 'apple', label: 'Apple' },
                { value: 'banana',  label: 'Banana' },
                { value: 'blueberry',  label: 'Blueberry' },
                { value: 'grapes',  label: 'Grapes' },
                { value: 'pineapple',  label: 'Pineapple' },
            ]}
        />
    )
}

Стилі

Для стилізації ми обираємо Tailwind CSS. Завдяки його компонентному підходу, для більшості проектів достатньо одного файлу app.css.

resources
└── css
    └── app.css

Для складніших проектів ми розділяємо стилі за папками base, components та utilities.

resources
└── css
    ├── base
    │   ├── a.css
    │   └── h1.css
    ├── components
    │   ├── button.css
    │   └── card.css
    ├── utilities
    │   └── typography.css
    └── app.css

Багатозонні застосунки

Якщо застосунок має чітко розділені частини (наприклад, адмін-панель та клієнтську частину), ми вводимо папку apps. Кожна «зона» може мати власні modules та pages, а спільний дизайн-системний код залишається у common у корені resources/js.

resources
├── css
│   ├── admin
│   │   └── app.css
│   └── client
│       └── app.css
└── js
    ├── apps
    │   ├── admin
    │   │   ├── modules
    │   │   ├── pages
    │   │   └── app.tsx
    │   └── client
    │       ├── modules
    │       ├── pages
    │       └── app.tsx
    ├── common
    └── modules

Нотатки щодо React-компонентів

Ось як виглядає типовий компонент у наших проектах:

import { PropsWithChildren } from 'react';

import { cn } from '@/common/helpers/cn';
import { PropsWithClassName } from '@/common/types/props';

type Props = PropsWithClassName<PropsWithChildren<{
    onClick?: () => void;
    type?: 'button' | 'submit';
}>>;

export function Button({ onClick, type, className, children }: Props) {
    return (
        <button type={type} onClick={onClick} className={cn(className)}>
            {children}
        </button>
    );
}
  • Ми віддаємо перевагу оголошенню компонентів через function, а не через const, щоб візуально відрізняти їх від змінних. Анонімні функції (колбеки) пишемо як стрілкові.
  • Використовуємо лише іменовані експорти для консистентності назв. Один файл — один компонент. Виняток — export default для сторінок Inertia, оскільки вона завантажує їх за назвою файлу.
  • Ми не використовуємо barrel files (index.ts). Хоча це здається зручним, на практиці це лише ускладнює навігацію та підтримку коду.
  • Імпорти групуються за допомогою prettier-plugin-sort-imports: спочатку бібліотеки, потім внутрішні модулі через абсолютні шляхи з аліасами.
  • Props сортуємо за алфавітом, залишаючи className та children наприкінці.

Оптимізація для backend та fullstack-розробки

У нашій компанії на одного frontend-розробника припадає чотири backend-фахівці. Таке співвідношення навчило нас будувати процеси так, щоб backend-розробники могли створювати інтерфейси самостійно.

Саме тому поділ на common/modules та pages є надзвичайно ефективним. Колеги з backend використовують готові абстракції в pages, не заглиблюючись у тонкощі React. Якщо ж фіча потребує складної логіки чи управління станом, підключається фахівець із frontend. Такий підхід дозволяє backend-розробникам закривати до 75% завдань зі створення форм та таблиць, використовуючи готові патерни.

Популярні

Інше, що варто прочитати

32 Оновлено 26 червня, 2026

Інтеграція Laravel Socialite з бібліотекою Google Client PHP

Ви хочете навчитися, як інтегрувати Google OAuth у вашому проекті Laravel, використовуючи Socialite? Дізнайтеся, як налаштувати доступ до сервісів Google, таких як Календар, у нашій сьогоднішній статті

14 Оновлено 26 червня, 2026

Локальні моделі та їх скоупи в Laravel за допомогою атрибута Scope

В Laravel 12 ми отримали можливість використовувати новий підхід для визначення локальних скоупів у моделях Eloquent. Дізнайтеся, як новий атрибут #[Scope] спрощує цей процес і зберігає ваші назви методів незмінними

17 Оновлено 26 червня, 2026

Обробка геопросторових даних за допомогою Laravel Magellan

Ви готові відкрити нові горизонти у роботі з геопросторовими даними в Laravel? Дізнайтеся, як за допомогою PostGIS та пакету Laravel-Magellan можна легко зберігати, запитувати та маніпулювати інформацією про розташування, перетворюючи ваші проекти на вражаючі рішення у сфері картографії та геолокації!