Architektura i technikalia projektu
View on GitHubArchitektura frontendu i atomic design
Jak URL staje się wyrenderowaną stroną
Browser requests /projects/5
│
▼
routes/web.php → ProjectController::show()
- Inertia::render('Projects/Show', [ ...props ])
│
▼
Inertia (server) serializes props, picks the component name 'Projects/Show'
│
▼ (full page load) ▼ (subsequent SPA-style visit)
resources/views/app.blade.php Inertia (client) swaps the
- @inertia directive renders a <div id="app"> page component in place,
with the initial page data as a data-page attr no full reload
│
▼
resources/js/app.tsx's createInertiaApp({ resolve })
- resolve('Projects/Show') → resolvePageComponent(...)
- dynamically imports resources/js/Pages/Projects/Show.tsx via
import.meta.glob('./Pages/**/*.tsx') — every .tsx file under Pages/
is a candidate, matched by its path
│
▼
<Component {...pageProps} /> renders inside the fixed provider stack (see below)
Nie ma żadnego klienckiego routera dopasowującego wzorce URL-i do komponentów — string nazwy strony, jaki wysyła Laravel ('Projects/Show'), to jedyna rzecz, która decyduje o tym, jaki plik się renderuje, i mapuje się bezpośrednio na ścieżkę resources/js/Pages/Projects/Show.tsx. Nazwanie nowej strony oznacza stworzenie pliku pod pasującą ścieżką; nic więcej nie musi być rejestrowane.
Stos providerów, pod którym montuje się każda strona
Plik: resources/js/app.tsx
<ThemeProvider>
<AccentProvider>
<ModalProvider>
<AlertProvider>
<ShortcutProvider>
<ModalContainer />
<Component {...pageProps} key={key} />
<OnboardingGate />
</ShortcutProvider>
</AlertProvider>
</ModalProvider>
</AccentProvider>
</ThemeProvider>
Kolejność zagnieżdżenia ma znaczenie, nie jest arbitralna: AccentProvider wywołuje useTheme() wewnętrznie (jego matematyka nieprzezroczystości różni się per rozstrzygnięty motyw — zobacz ../accent-colors/README.md), więc musi siedzieć wewnątrz ThemeProvider; każdy inny provider jest niezależny i teoretycznie mógłby zagnieżdżać się w dowolnej kolejności względem siebie. Dodaj nowy przekrojowy context tutaj, na poziomie, jakiego potrzebuje (wewnątrz ThemeProvider tylko jeśli też potrzebuje useTheme(), w przeciwnym razie gdziekolwiek w stosie) — nie wewnątrz konkretnej Page czy Layoutu, inaczej każda inna strona traci do niego dostęp.
OnboardingGate (zdefiniowany inline w app.tsx, nie context) odczytuje dwie flagi onboardingu auth.user ze współdzielonych propów Inertii (zobacz poniżej) i renderuje blokujący modal nad wszystkim innym, dopóki obie nie są ukończone — zobacz ../project-onboarding/README.md.
Współdzielone propy vs. propy per strona
Plik: app/Http/Middleware/HandleInertiaRequests.php
public function share(Request $request): array
{
return [
...parent::share($request),
'auth' => [
'user' => $request->user() ? [
'id' => $request->user()->id,
'name' => $request->user()->name,
'email' => $request->user()->email,
'avatar' => $request->user()->avatar,
'has_completed_onboarding' => $request->user()->has_completed_onboarding,
'has_completed_project_onboarding' => $request->user()->has_completed_project_onboarding,
'session_lifetime' => $request->user()->session_lifetime,
] : null,
],
'hasProjects' => fn () => $request->user()
? $this->projectService->hasAnyProjectsForUser($request->user()->id)
: true,
'emailEnabled' => fn () => $this->mailConfigurationService->isEnabled(),
'flash' => [
'success' => fn () => $request->session()->get('success'),
'error' => fn () => $request->session()->get('error'),
'warning' => fn () => $request->session()->get('warning'),
'information' => fn () => $request->session()->get('information'),
'action_url' => fn () => $request->session()->get('action_url'),
],
'notifications' => fn () => $request->user()
? $this->notificationService->getAllForUser($request->user()->id)
: [],
];
}
Każdy prop tutaj (auth, hasProjects, emailEnabled, flash, notifications) dociera do każdej strony automatycznie, odczytywany przez usePage<PageProps>().props — tak właśnie alerty i popup powiadomień dostają swoje dane, bez powtarzania tego samego propa przez kontroler każdej strony. Forma domknięcia (fn () => ...) to wbudowana leniwość Inertii "ewaluuj to tylko, jeśli faktycznie potrzebne" — tanie propy jak auth to zwykłe tablice, ale wszystko wymagające zapytania (hasAnyProjectsForUser, getAllForUser) jest owinięte, żeby nie uruchamiało się przy każdym pojedynczym żądaniu niezależnie od tego, czy strona z tego korzysta. Dodaj tutaj nowy prop tylko wtedy, gdy faktycznie każda strona go potrzebuje — prop specyficzny dla strony należy zamiast tego do wywołania Inertia::render(...) we własnej akcji kontrolera tej strony.
Atomic design: co gdzie należy
- Atoms (
Components/Atoms/) — najmniejsze reużywalne elementy:Button,Badge,Input,Icon,BrandIcon. Przyjmują prymitywne propy (string,boolean, klucz wariantucva), nigdy nie importują typu zresources/js/types/dla modelu domenowego, nigdy nie wywołują hooka sięgającego poza własne propy (żadnegouseAccent(), żadnegorouter.post(...)). - Molecules (
Components/Molecules/) — złożone z Atoms w mały, wciąż dość generyczny element:Breadcrumb,NotificationItem,RoleBadge. Mogą przyjmować prop w kształcie domenowym (AlertItem,WorkspaceRole), ale wciąż same nie sięgają do globalnego stanu — wszystko przychodzi jako propy od jakiegoś Organisma, który je renderuje. - Organisms (
Components/Organisms/) — specyficzne dla funkcji, złożone z Molecules, i pierwszy poziom, któremu wolno wywoływać hooki contextu bezpośrednio (useAlert(),useAccent()) i samodzielnie robić wywołaniarouter.post(...)—IssuePageHeader,WorkspaceSettingsContent,NotificationsPopupto wszystko Organisms dokładnie z tego powodu. - Pages (
Pages/) — jeden na nazwę strony Inertii, otrzymujący propy kontrolera bezpośrednio i składający Organisms (plus okazjonalny Molecule) w faktyczny ekran. Page to jedyny poziom, który powinien destrukturyzować propy strony Inertii jako sygnaturę swojego komponentu.
Sygnał, że "to jest na złym poziomie": jeśli Atom potrzebuje importu typu domenowego albo hooka contextu, to w rzeczywistości jest Molecule albo Organism w przebraniu — przenieś w górę zamiast dodawać import.
Gdzie żyje reszta
context/— jeden plik na przekrojowy temat (AlertContext,ThemeContext,AccentContext,ModalContext,ShortcutContext), każdy eksportujący<X>Provideri hookuseX(), który rzuca wyjątek, jeśli wywołany poza swoim providerem (zobaczuseX()każdego istniejącego contextu po dokładnie ten kształt zabezpieczenia).hooks/— reużywalna logika ze stanem, która nie jest całym contextem (useSavedFilters, hook resize'owalnej kolumny) — granica między "to potrzebuje hooka" a "to potrzebuje contextu" to to, czy więcej niż jedna niepowiązana część drzewa potrzebuje tego samego stanu, czy tylko tego samego kształtu logiki niezależnie.types/— jeden plik na model/enum backendu, jaki odzwierciedla (Issues.ts,Projects.ts,Roles.ts, …) — zobacz dowolny przewodnik wdocumentation/en/permissions/albonotifications/po ustaloną konwencję ręcznego trzymania kształtu typu frontendu w zgodzie z odpowiednikiem backendu (nie ma codegenu).utils/— czyste funkcje bez żadnej zależności od React/Inertii (cn.ts,colors.ts,time.ts,accentColors.ts) — jeśli "util" potrzebujeuseStatealbo contextu, należy zamiast tego dohooks/.
Testy
Ten przewodnik to materiał referencyjny. Po same konwencje testowania (skolokowane *.test.tsx, Vitest + Testing Library, jsdom), zobacz top-level README.md i CLAUDE.md.
