---
title: Inertia + React Integration
type: concept
created: 2026-09-06
updated: 2026-09-06
sources: [codebase snapshot 2026-09-06]
tags: [inertia, react, frontend, ssr, zustand, zod]
---

# Inertia + React Integration

The frontend is Inertia v3 + React 19, served by Laravel. Server state flows down via Inertia props; only UI/filter state lives client-side in Zustand; input-boundary validation uses Zod.

> **Note on provenance:** reflects the code as of 2026-09-06; no written ADR exists.

## App bootstrap

`resources/js/app/app.tsx` calls `createInertiaApp` and wraps the app in a `<Providers>` component (currently minimal, reserved for future query client/theme/router). `app/bootstrap.ts` imports the global stylesheet. `app/providers/index.tsx` must never import from `domains/`.

## Page resolution

Inertia pages live in `resources/js/pages/` and are resolved by string. `config/inertia.php` sets the page path to `resource_path('js/pages')` and enables **SSR**. Pages are referenced as:

- `Route::inertia('/', 'welcome')` → `pages/welcome.tsx`
- `Route::inertia('/users', 'users/index', [...])` → `pages/users/index.tsx`

> Updated 2026-09-06: the issue-tracking port replaced the sample pages with real controller-driven pages. Current pages: `auth/login`, `workspace/overview`, `workspace/issues/index`, `workspace/issues/show`, `workspace/time`, `workspace/activity`, `workspace/labels`, `workspace/settings`, `workspace/empty`. Mutations use the Inertia router (`router.post/put/delete`) against `Web\*` controllers; the `welcome` page and `/users` route were removed.

## Shared props

`app/Http/Middleware/HandleInertiaRequests.php` extends Inertia's `Middleware` and shares:

```php
'name' => config('app.name'),
'auth' => ['user' => $request->user()],
```

The frontend types these shared props in `resources/js/types/global.d.ts` (`InertiaConfig.sharedPageProps`). The `auth` guard for web is not yet applied, so `$request->user()` is likely null in practice.

## Frontend module conventions

- `resources/js/app/` — providers, bootstrap, global stores (e.g. `ui.store.ts` for the sidebar toggle).
- `resources/js/components/` — atomic hierarchy: `atoms/`, `molecules/`, `organisms/`, `templates/`, plus `ui/` primitives (button, input, label from Radix + CVA).
- `resources/js/domains/<name>/` — a **domain module** with `hooks/`, `schemas/`, `stores/`, `types/`. Only `domains/users` exists today (a sample users list with a filter). The domain's `index.ts` re-exports its public surface.
- `resources/js/lib/utils.ts` — `cn()` helper (clsx + tailwind-merge).
- `resources/js/types/` — global/shared types.

## State ownership

A strong, explicit convention: **server records never live in client state.** From `domains/users/stores/user-filters.store.ts`:

> "Only filter/UI state lives here. The user _records_ are server/Inertia state and must never be stored in Zustand."

The users page reads records from `usePage<Props>().props.users` and drives a local filter via `useUserFilters()`.

## Routing separation

`routes/web.php` (Inertia) and `routes/api.php` (JSON, `/api/v1`) are fully separate stacks. Sanctum is installed and the token guard configured, but `auth:sanctum` is **not yet applied** to the API routes (see `bootstrap/app.php` comment).

## Observations / open questions

- The sample `users` page is **not connected** to the backend `User` domain — it uses hardcoded `id: number` records (see [user](../entities/user.md)).
- SSR is enabled (`config/inertia.php`) but there is no SSR bundle configured in that file; the `build:ssr` script exists.
- `vite-plus` (check/fmt) and the React Compiler are enabled; Tailwind v4 is used.

## Related pages

- [cqrs](cqrs.md) — controllers returning DTOs to Inertia
- [validation-strategy](validation-strategy.md) — client-side Zod vs. server FormRequest
- [user](../entities/user.md) — the sample users page vs. the domain User