# Laravel en Inertia

React-pagina's op je Laravel-controllers, zonder API ertussen. En wanneer je toch Blade of een losse frontend kiest.

Multimedia & Creatieve Technologie · 3.1 The Agency · Zelfstudie · versie 1.0.0 · 2026-10-02

Tags: laravel, php, inertia, react, architecture

Slides: <https://slides.mctantwerp.be/nl/the-agency/22-inertia>

<!-- slide 2 -->

## Agenda

1. Drie manieren
2. Wat is Inertia
3. Een request van dichtbij
4. Opzetten
5. Pagina's en links
6. Formulieren
7. Gedeelde en trage data
8. Wanneer wat

> **Toelichting:** Het eerste hoofdstuk gaat nog niet over Inertia, maar over de vraag waar het een antwoord op is. Hoofdstuk 2 tot 4 tonen hoe Inertia werkt en hoe je het opzet. Daarna komt het dagelijkse werk: pagina's, formulieren en data. Het laatste hoofdstuk helpt je kiezen bij een klantproject.

<!-- slide 3 -->

## Na deze les kan je

- Uitleggen hoe Blade, een losse React-app met een API en Inertia een pagina in de browser krijgen
- Kiezen tussen die drie voor een klantproject, en die keuze verdedigen
- Een pagina maken met Inertia::render en een React-component met props
- Navigeren met Link en Wayfinder, zonder volledige page reload
- Formulieren met validatie en flash messages bouwen, zoals je dat in Blade gewoon bent
- Trage data uitstellen met deferred props en partial reloads

> **Toelichting:** Dit is ook de checklist voor de opdracht op het einde. SSR en optimistic updates komen kort aan bod: je moet weten dat ze bestaan en wanneer je ze nodig hebt, niet elk detail.

<!-- slide 4 -->

## 1. Drie manieren

> **Toelichting:** Elk Agency-project begint met dezelfde vraag: hoe komt de HTML in de browser? Het antwoord bepaalt hoeveel projecten je hebt, hoe login werkt en hoeveel code je dubbel schrijft. Inertia snap je pas goed als je weet tussen welke twee het zit.

<!-- slide 5 -->

**Wie maakt de HTML?**

Bij Blade doet de server het. Bij een losse React-app doet de browser het, met data uit een API. Inertia laat de browser renderen, met data rechtstreeks uit je controller.

> **Toelichting:** Hou deze vraag vast voor de rest van het hoofdstuk. De verschillen die volgen, zoals login, validatie en SEO, komen er allemaal uit voort.

<!-- slide 6 -->

### Blade

*ProjectController*

```php
public function index()
{
    return view('projects.index', [
        'projects' => Project::latest()->get(),
    ]);
}
```

*projects/index.blade.php*

```php
@foreach ($projects as $project)
  <a href="{{ route('projects.show', $project) }}">
    {{ $project->name }}
  </a>
@endforeach
```

De controller haalt data op, de view maakt er HTML van, de browser toont ze. Elke klik is een nieuw request en een volledig nieuwe pagina. Eenvoudig, snel bij het eerste bezoek en goed leesbaar voor Google. Maar alles wat zonder reload moet gebeuren, vraagt JavaScript ernaast.

> **Toelichting:** Dat JavaScript ernaast is waar Blade-projecten rommelig worden: een stukje Alpine hier, een fetch daar, en data die je zowel in PHP als in JavaScript moet kennen. Voor een site met vooral content is dat geen probleem. Voor een dashboard met filters, modals en formulieren die op elkaar reageren, wordt het dat wel.

<!-- slide 7 -->

### Een losse frontend

- **Frontend**: Een React-app met Vite of Next.js. Eigen repo, eigen deploy, eigen urls met React Router.
- **Backend**: Laravel met enkel `routes/api.php`, API Resources en Sanctum voor de login. Geen views.

De browser krijgt een lege pagina en een bundle JavaScript. React start, toont een laadscherm, en vraagt dan pas de data aan je API. Alles wat de gebruiker ziet, maakt de browser. Laravel levert enkel JSON.

Meer in de docs: [Sanctum SPA authentication](https://laravel.com/docs/13.x/sanctum#spa-authentication)

> **Toelichting:** Met Next.js of React Router in framework mode haal je data op een server op, en verdwijnt een deel van de code op de volgende slide. Dan draai je wel een Node-server naast je Laravel-app, en heb je nog altijd twee projecten met een API ertussen.

<!-- slide 8 -->

### Data ophalen in React

```tsx
export default function Projects() {
  const [projects, setProjects] = useState<Project[]>([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetch('/api/projects', { headers: { Accept: 'application/json' } })
      .then((response) => response.json())
      .then((json) => setProjects(json.data))
      .finally(() => setLoading(false))
  }, [])
  return loading ? <Spinner /> : <ProjectList projects={projects} />
}
```

Dit schrijf je voor elke pagina. De data bestaat al op de server, maar React haalt ze een tweede keer op, nadat de pagina geladen is.

> **Toelichting:** In de praktijk neem je er TanStack Query bij, voor caching, opnieuw proberen en fouten. Dat lost veel op, maar het is nog een library om te leren en te configureren. Vergelijk deze code straks met de Inertia-pagina in hoofdstuk 2.

<!-- slide 9 -->

### Wat een losse frontend kost

- Twee projecten en twee deploys, en je urls staan twee keer: in React Router en in routes/api.php.
- Login over twee origins: Sanctum met cookies, CORS en een CSRF-cookie, of tokens die ergens in de browser moeten staan.
- Elke pagina haalt zijn data zelf op, dus overal een laadstatus, een foutstatus en een useEffect.
- Validatiefouten komen als 422 met JSON terug, en je zet ze zelf bij het juiste veld.
- Een API Resource per model, en een contract dat je niet zomaar mag breken.

> **Toelichting:** Daar staat iets tegenover: dezelfde API kan ook een mobiele app of de systemen van de klant bedienen. Heeft je project die andere clients, dan betaal je deze prijs graag. Heeft het ze niet, dan bouw je een API voor één client die je zelf schrijft. De lessen over API's en authenticatie gaan dieper in op Sanctum en resources.

<!-- slide 10 -->

**Een SPA zonder API.**

Je schrijft routes en controllers zoals voor Blade. Alleen geeft de controller geen view terug, maar de naam van een React-component en zijn props.

> **Toelichting:** SPA staat voor single-page application: de browser laadt één keer een pagina en de JavaScript, en wisselt daarna enkel de inhoud. Zo voelt een Inertia-app voor de gebruiker. Voor jou als developer voelt het als een gewone Laravel-app.

<!-- slide 11 -->

### Wat er over het netwerk gaat

- Blade, eerste bezoek: GET /projects geeft 200 text/html. Toont de HTML zoals ze binnenkomt. De lijst staat er meteen, ook zonder JavaScript.
- Blade, klik op een link: GET /projects/12 geeft 200 text/html. Gooit de hele pagina weg en bouwt ze opnieuw op: head, nav en footer inbegrepen. De scrollpositie en alle state zijn weg.
- React + API, eerste bezoek: GET /projects geeft 200 text/html, daarna GET /api/projects geeft 200 application/json. Toont eerst een lege pagina, start React, toont een laadscherm, en rendert de lijst pas na het tweede request.
- React + API, klik op een link: GET /api/projects/12 geeft 200 application/json. React Router past de url aan, de nieuwe pagina vraagt haar data, en React rendert ze. Welke component bij welke url hoort, weet de frontend zelf.
- Inertia, eerste bezoek: GET /projects geeft 200 text/html. Start React, dat de component projects/index meteen rendert met de props uit de HTML. Geen tweede request voor data.
- Inertia, klik op een link: GET /projects/12 geeft 200 application/json. Wisselt de component, past de url aan en laat de rest van de pagina staan. Welke component het wordt, besliste je controller.

> **Toelichting:** Let vooral op het eerste bezoek. Bij de losse React-app komt er eerst een lege pagina, en pas daarna de data. Bij Inertia zit de data al in de eerste HTML. Bij een klik stuurt Inertia JSON, net als een API, maar de vorm van die JSON is altijd dezelfde: een component en zijn props. Open daarna je eigen app met de network tab open, en kijk of je hetzelfde ziet.

<!-- slide 12 -->

### Naast elkaar

|   | Blade | React + API | Inertia |
| --- | --- | --- | --- |
| HTML gemaakt door | De server | De browser | De browser |
| Routes | `web.php` | Twee keer | `web.php` |
| Data naar de pagina | Variabelen in de view | fetch naar de API | Props uit de controller |
| Login | Sessie | Sanctum en CORS | Sessie |
| Validatiefouten | Vanzelf | Zelf verwerken | Vanzelf |
| Projecten | Eén | Twee | Eén |
| Andere clients | Nee | Ja | Nee, tenzij je een API toevoegt |

> **Toelichting:** De rij Login is waar in een losse frontend de meeste uren verdwijnen. Bij Inertia werkt auth zoals in een Blade-app: de sessie, de cookie, de middleware auth. Daarom heb je in een Inertia-app geen Sanctum nodig, zie ook de les over authenticatie.

<!-- slide 13 -->

### Een klant wil een dashboard voor zijn team, enkel in de browser, met veel filters en formulieren. Wat kies je?

- A. Blade met wat JavaScript
- B. Een losse React-app met een Laravel API
- C. Inertia met React
- D. Twee Laravel-apps

**Antwoord:** C. Inertia met React

Inertia geeft je React voor de interactie, en houdt routing, login en validatie in Laravel. Een API is overbodig zolang er geen andere client is: je bouwt dan een contract voor één frontend die je zelf schrijft. Blade kan ook, maar bij een dashboard vol filters en modals groeit het JavaScript ernaast snel uit tot een tweede app zonder structuur.

> **Toelichting:** Komt er later toch een mobiele app, dan voeg je routes/api.php toe naast je Inertia-pagina's. De logica zit in actions of services, en die gebruiken ze allebei. Daarover gaat het laatste hoofdstuk.

<!-- slide 14 -->

## 2. Wat is Inertia

> **Toelichting:** Inertia is klein, en dat is net de bedoeling. Het vervangt niets van Laravel en niets van React. Het is de lijm ertussen. Wie dat snapt, zoekt niet naar features die er niet zijn, zoals state management of een router in React.

<!-- slide 15 -->

### Inertia in het kort

- Gemaakt door Jonathan Reinink in 2019, nu onderhouden door het team van Laravel.
- Geen framework, maar een protocol: een adapter op de server en een in de browser die dezelfde JSON spreken.
- Adapters voor Laravel en Rails op de server, en voor React, Vue en Svelte in de browser.
- Inertia 3 is de huidige versie, en zit in de React-, Vue- en Svelte-starter kits van Laravel 13.

> **Toelichting:** Inertia 3 vraagt PHP 8.2, Laravel 11 of hoger en React 19. Tutorials over Inertia 1 en 2 zijn grotendeels nog bruikbaar: render, Link, useForm en shared data werken hetzelfde. Wat veranderde: axios zit er niet meer in, een Vite-plugin vindt je pagina's zelf, en useHttp en optimistic updates zijn nieuw. Twijfel je bij een tutorial, kijk dan in de upgrade guide.

<!-- slide 16 -->

### Van view naar render

*Blade*

```php
return view('projects.index', [
    'projects' => Project::latest()->get(),
]);
```

*Inertia*

```php
return Inertia::render('projects/index', [
    'projects' => Project::latest()->get(),
]);
```

Eén regel verschilt. `projects/index` is de naam van een component in `resources/js/pages`, de array worden de props van die component. Routes, route model binding, policies, Form Requests en middleware blijven precies wat ze waren.

Meer in de docs: [How it works](https://inertiajs.com/docs/v3/core-concepts/how-it-works)

> **Toelichting:** Er is ook een helper: inertia('projects/index', [...]) doet hetzelfde. Voor een pagina zonder data, zoals een about-pagina, schrijf je Route::inertia('/about', 'about') in routes/web.php, zonder controller.

<!-- slide 17 -->

### De pagina

*resources/js/pages/projects/index.tsx*

```tsx
import { Link } from '@inertiajs/react'

type Props = { projects: { id: number; name: string }[] }

export default function Index({ projects }: Props) {
  return projects.map((project) => (
    <Link key={project.id} href={`/projects/${project.id}`}>
      {project.name}
    </Link>
  ))
}
```

1. Het type van een project. TypeScript weet niet wat je controller stuurt, dus dat schrijf je zelf.
2. De props zijn de array uit `Inertia::render()`. Dezelfde sleutel, `projects`.
3. `Link` gaat naar de volgende pagina zonder page reload. Daarover gaat hoofdstuk 5.

> **Toelichting:** Vergelijk met Data ophalen in React uit hoofdstuk 1. Geen useState voor de data, geen useEffect, geen laadscherm. Wanneer de component rendert, zijn de props er al. De default export is belangrijk: Inertia zoekt die in het bestand.

<!-- slide 18 -->

### Props zijn publiek

```php
// Te veel: elke kolom van het model
'project' => $project,

// Beter: enkel wat de pagina toont
'project' => $project->only('id', 'name', 'client'),
```

> [!WARNING]
> Alles in de props staat als JSON in de paginabron en in de network tab. Geef je een model mee, dan krijgt de browser elke kolom die niet in `$hidden` staat: interne notities, het budget, een token.

Bij Blade bleef wat de view niet toonde op de server. Bij Inertia gaat alles mee wat in de array staat, of de pagina het nu gebruikt of niet.

> **Toelichting:** Wil je dezelfde vorm op meerdere pagina's, dan kan je ook een API Resource als prop meegeven, ook al heb je geen API. Kijk dan in de network tab of je object onder een data-key terechtkomt. Rechten naar de frontend sturen is een apart geval, daarover gaat de les over autorisatie.

<!-- slide 19 -->

## 3. Een request van dichtbij

> **Toelichting:** Inertia lijkt magie tot je één keer in de network tab kijkt. Dan zie je gewone requests met één extra header. Wie weet hoe die eruitzien, debugt een Inertia-app zoals elke andere webapp.

<!-- slide 20 -->

### Van eerste bezoek tot klik

1. Browser → Laravel, **GET /projects**: Het eerste bezoek: een gewoon request, zonder speciale header. Laravel draait de route en de controller zoals altijd.
2. Laravel → Browser, **HTML met de page object**: Een volledige HTML-pagina met je JavaScript, en de component en props als JSON in een script-tag.
3. Browser → Gebruiker, **React toont projects/index**: De adapter leest die JSON, laadt de component en rendert hem met de props. Geen tweede request voor data.
4. Gebruiker → Browser, **Klik op een Link**: Link vangt de klik op. De browser laadt geen nieuwe pagina.
5. Browser → Laravel, **XHR naar /projects/12**: Een XHR-request met de header X-Inertia: true. Zo weet Laravel dat de client JSON wil.
6. Laravel → Browser, **JSON: component en props**: Dezelfde route en controller, maar nu komt er enkel een page object terug: de component, de props, de url en de versie van je assets.
7. Browser → Gebruiker, **projects/show, nieuwe url**: React wisselt de component, Inertia zet de url in de adresbalk. De terugknop werkt zoals je verwacht.

> **Toelichting:** Open je eigen app met de network tab open, filter op Fetch/XHR en klik door je app. Je ziet bij elke klik één request naar een gewone url, met X-Inertia in de headers en een page object als antwoord. Het eerste bezoek zie je onder Doc.

<!-- slide 21 -->

### Het eerste bezoek

```html
<html>
<head>
  <script type="module" src="/build/assets/app-4f2a91.js"></script>
</head>
<body>
  <script data-page="app" type="application/json">
    {"component":"projects/index","props":{"errors":{},"projects":[…]},
     "url":"\/projects","version":"c32b8e49"}
  </script>
  <div id="app"></div>
</body>
</html>
```

Dit maakt `@inertia` in je root template. Een page object heeft altijd vier velden: `component`, `props`, `url` en `version`. Zonder JavaScript is de pagina leeg, en dat is ook wat een crawler zonder SSR te zien krijgt.

> **Toelichting:** Oudere versies van Inertia zetten die JSON in een data-page-attribuut op de div, vol "-entities. Zie je dat in een tutorial of een ouder project, dan is het hetzelfde idee. De \\/ in de JSON is geen typfout: zo kan een string met </script> erin de tag niet te vroeg sluiten.

<!-- slide 22 -->

### Elke klik daarna

```http
GET /projects/12
X-Inertia: true
X-Inertia-Version: c32b8e49

HTTP/1.1 200 OK
Content-Type: application/json
X-Inertia: true
Vary: X-Inertia

{"component":"projects/show","props":{"errors":{},"project":{"id":12,…}},"url":"/projects/12",…}
```

Dezelfde url, dezelfde controller. Inertia ziet de header `X-Inertia` en geeft JSON terug in plaats van HTML. De JSON-variant van elke pagina krijg je er zo gratis bij, en daarom heb je geen aparte API nodig.

Meer in de docs: [The protocol](https://inertiajs.com/docs/v3/core-concepts/the-protocol)

> **Toelichting:** Vary: X-Inertia zegt tegen de browser en elke cache dat dezelfde url twee soorten antwoorden heeft. Zonder die header kan het gebeuren dat de terugknop of een CDN de JSON toont in plaats van de pagina.

<!-- slide 23 -->

### Na een deploy

```http
GET /projects/12
X-Inertia: true
X-Inertia-Version: c32b8e49

HTTP/1.1 409 Conflict
X-Inertia-Location: https://app.agency.be/projects/12
```

Wie je app open heeft tijdens een deploy, draait nog de oude JavaScript. De middleware vergelijkt de versie van de client met die van de server, berekend uit het manifest van Vite. Verschillen ze, dan antwoordt Laravel met 409, en laadt de browser de pagina volledig opnieuw, met de nieuwe bundle.

Meer in de docs: [Asset versioning](https://inertiajs.com/docs/v3/advanced/asset-versioning)

> **Toelichting:** Hetzelfde antwoord, een 409 met X-Inertia-Location, maak je zelf met Inertia::location($url). Dat heb je nodig voor een redirect naar een andere site, zoals de betaalpagina van Mollie of een login bij Microsoft. Een gewone redirect()->away() werkt daar niet: de XHR van Inertia mag die andere site niet laden, en je krijgt een CORS-fout.

<!-- slide 24 -->

### Je klikt op een Link in een Inertia-app. Wat stuurt Laravel terug?

- A. Een volledige HTML-pagina
- B. JSON met de naam van een component en zijn props
- C. Enkel de props, zonder component
- D. Een stuk HTML dat React in de pagina zet

**Antwoord:** B. JSON met de naam van een component en zijn props

Een request met X-Inertia krijgt een page object als JSON: component, props, url en version. React rendert de component zelf, Laravel stuurt geen HTML. Het laatste antwoord lijkt op hoe Livewire en htmx werken: daar maakt de server stukjes HTML.

> **Toelichting:** Enkel bij het eerste bezoek, bij een gewone a-tag of na een 409 komt er een volledige HTML-pagina. Zie je die toch na een klik, kijk dan of je Link gebruikt.

<!-- slide 25 -->

## 4. Opzetten

> **Toelichting:** Voor een nieuw project neem je de starter kit: die zet alles klaar, met login erbij. Zelf installeren is goed om te weten wat de starter kit voor je doet, en voor een bestaand Blade-project dat je stap voor stap wil omzetten.

<!-- slide 26 -->

### De React starter kit

```console
$ laravel new agency-app
Which starter kit would you like to install?
› React
$ cd agency-app && composer run dev
Server running on http://127.0.0.1:8000
```

- Inertia 3, React 19, TypeScript en Tailwind 4.
- shadcn/ui voor knoppen, dialogs en formulieren, als code in je eigen project.
- Fortify voor login, registratie en twee-factor-authenticatie.
- Wayfinder voor je routes in TypeScript.

Meer in de docs: [Starter kits](https://laravel.com/docs/13.x/starter-kits#react)

> **Toelichting:** composer run dev start in één terminal de server van Laravel, Vite, de queue worker en de logs. Met Herd heb je enkel npm run dev nodig. Het installatieprogramma vraagt ook naar teams en WorkOS. Voor een eerste project kies je de ingebouwde login van Laravel, zonder teams.

<!-- slide 27 -->

### resources/js

```bash
resources/js/
├── app.tsx
├── components/
│   └── ui/
├── hooks/
├── layouts/
├── lib/
├── pages/
│   ├── auth/
│   ├── settings/
│   └── dashboard.tsx
└── types/
```

- `pages` heeft één bestand per pagina. Het pad is de naam in `Inertia::render()`.
- `components/ui` zijn de componenten van shadcn. Ze zijn van jou, pas ze gerust aan.
- `layouts` bevat de layout met sidebar of header, en die van de loginpagina's.
- `types` heeft de TypeScript-types voor je props en gedeelde data.

> **Toelichting:** Inertia::render('dashboard') zoekt resources/js/pages/dashboard.tsx, en 'settings/profile' zoekt pages/settings/profile.tsx. De starter kit gebruikt kleine letters, hou dat zo. Een typfout in de naam geeft pas een fout in de browser, niet in PHP. Een feature test met assertInertia vangt dat wel op.

<!-- slide 28 -->

### Zelf installeren

```console
$ composer require inertiajs/inertia-laravel
$ php artisan inertia:middleware
$ npm install @inertiajs/react @inertiajs/vite react react-dom @vitejs/plugin-react
```

*bootstrap/app.php*

```php
->withMiddleware(function (Middleware $middleware) {
    $middleware->web(append: [HandleInertiaRequests::class]);
})
```

De middleware `HandleInertiaRequests` deelt data met elke pagina, zet de validatiefouten in de props en controleert de versie. Ze hoort in de web-groep: Inertia draait op sessies, net als Blade.

Meer in de docs: [Server-side setup](https://inertiajs.com/docs/v3/installation/server-side-setup)

> **Toelichting:** Zonder de middleware werkt render() nog, maar ontbreken de gedeelde data, de validatiefouten en de versiecheck. Zie je ooit een modal met een volledige HTML-pagina erin, dan kreeg een request geen Inertia-response terug: een dd(), een foutpagina van Laravel of een redirect naar een route die geen Inertia gebruikt.

<!-- slide 29 -->

### De client

*vite.config.ts*

```ts
import { defineConfig } from 'vite'
import laravel from 'laravel-vite-plugin'
import react from '@vitejs/plugin-react'
import inertia from '@inertiajs/vite'

export default defineConfig({
  plugins: [
    laravel({ input: ['resources/js/app.tsx'] }),
    react(),
    inertia(),
  ],
})
```

*resources/js/app.tsx*

```tsx
import { createInertiaApp } from '@inertiajs/react'

createInertiaApp()
```

De Vite-plugin van Inertia 3 zoekt je pagina's zelf in de map `pages` en start de app. Daarom is `app.tsx` zo kort.

Meer in de docs: [Client-side setup](https://inertiajs.com/docs/v3/installation/client-side-setup)

> **Toelichting:** In Inertia 2 en ouder stond hier een resolve-functie met import.meta.glob en een setup-functie met createRoot. Die code zie je nog in veel tutorials en oudere projecten. Ze werkt nog, maar met de Vite-plugin heb je ze niet meer nodig. In createInertiaApp() kan je opties meegeven, zoals title om de naam van je app achter elke paginatitel te zetten.

<!-- slide 30 -->

### De root template

*resources/views/app.blade.php*

```php
<!doctype html>
<html lang="nl">
<head>
    <meta charset="utf-8">
    @vite('resources/js/app.tsx')
    @inertiaHead
</head>
<body>
    @inertia
</body>
</html>
```

- `@vite` laadt je JavaScript en CSS.
- `@inertiaHead` zet de tags uit de Head-component erin, bij SSR.
- `@inertia` maakt de div en de script-tag met de page object.
- Het is de enige Blade-view in een Inertia-app.

> **Toelichting:** Wil je iets op elke pagina in de head, zoals een favicon, fonts of het script van Umami, dan komt het hier. Wat per pagina verschilt, zoals de title, zet je in React met de Head-component.

<!-- slide 31 -->

## 5. Pagina's en links

> **Toelichting:** Een Inertia-pagina is een gewone React-component. Wat je over React weet, blijft gelden: state, hooks, componenten. Nieuw zijn een handvol dingen van Inertia: props uit je controller, Head, layouts, Link en de router.

<!-- slide 32 -->

### Props met een type

*types/index.d.ts*

```ts
export type Project = {
  id: number
  name: string
  client: string
  deadline: string | null
}
```

Het type moet kloppen met de array in `render()`, en niemand controleert dat voor je. Een datum komt als string binnen, want JSON kent geen datums.

*pages/projects/show.tsx*

```tsx
import { Head } from '@inertiajs/react'
import type { Project } from '@/types'

type Props = { project: Project }

export default function Show({ project }: Props) {
  return (
    <>
      <Head title={project.name} />
      <h1>{project.name}</h1>
    </>
  )
}
```

> **Toelichting:** Er bestaan packages die TypeScript-types genereren uit je PHP-klassen, zoals spatie/laravel-typescript-transformer. Voor een klein project schrijf je ze met de hand, in resources/js/types. Head zet de title en meta-tags in de head van de pagina, ook als je de pagina wisselt.

<!-- slide 33 -->

### Een persistente layout

```tsx
import AppLayout from '@/layouts/app-layout'

export default function Index({ projects }: Props) {
  return <ProjectList projects={projects} />
}

Index.layout = (page: React.ReactNode) => <AppLayout>{page}</AppLayout>
```

Met `.layout` blijft de layout staan als je naar een andere pagina gaat. Enkel de pagina erin wisselt: een sidebar houdt zijn scrollpositie, een audiospeler speelt door. Zet je de layout in de JSX van de pagina zelf, dan bouwt React ze bij elke klik opnieuw op.

Meer in de docs: [Layouts](https://inertiajs.com/docs/v3/the-basics/layouts)

> **Toelichting:** In createInertiaApp() kan je ook een standaardlayout zetten met de optie layout: een functie die de naam van de pagina krijgt en een layout teruggeeft. Handig als bijna elke pagina dezelfde layout heeft. Kijk in resources/js/app.tsx en de map layouts van de starter kit hoe het daar gebeurt.

<!-- slide 34 -->

### Link

```tsx
import { Link } from '@inertiajs/react'

<Link href="/projects/12">Herfstcampagne</Link>

<Link href="/projects?status=active" preserveScroll>Actief</Link>

<Link href="/logout" method="post" as="button">Uitloggen</Link>
```

`Link` maakt een gewone a-tag, maar vangt de klik op en haalt de volgende pagina als JSON. `preserveScroll` laat de pagina op dezelfde hoogte staan. Met `method` wordt het een POST, PUT of DELETE, en dan hoort het een button te zijn.

Meer in de docs: [Links](https://inertiajs.com/docs/v3/the-basics/links)

> **Toelichting:** Met prefetch laadt Link de volgende pagina al als de muis erover hangt: <Link href=... prefetch>. Handig in een menu, niet in een lijst met honderd links. Een link naar een andere site, of naar een bestand om te downloaden, blijft een gewone a-tag.

<!-- slide 35 -->

### Wayfinder

```tsx
import { show } from '@/actions/App/Http/Controllers/ProjectController'
import { dashboard } from '@/routes'

show(12)       // { url: '/projects/12', method: 'get' }
show.url(12)   // '/projects/12'

<Link href={show(project.id)}>{project.name}</Link>
<Link href={dashboard()}>Dashboard</Link>
```

Wayfinder maakt van elke controller-methode en elke named route een TypeScript-functie. Hernoem je een route of verander je een parameter, dan faalt de build in plaats van een 404 bij de klant. Het zit in de starter kit, maar is nog in beta.

Meer in de docs: [laravel/wayfinder](https://github.com/laravel/wayfinder)

> **Toelichting:** De bestanden in resources/js/actions en resources/js/routes zijn gegenereerd. De Vite-plugin van Wayfinder maakt ze opnieuw als je een route of controller aanpast. Pas ze dus nooit met de hand aan. Zet je een feature uit in Fortify, zoals registratie, dan moet je ook de imports van die routes weghalen, anders faalt de build.

<!-- slide 36 -->

### De router

```tsx
function filter(status: string) {
  router.get('/projects', { status }, {
    preserveState: true,
    replace: true,
  })
}
```

1. Een GET naar `/projects?status=active`. Het tweede argument wordt de querystring.
2. `preserveState` houdt de state van je component, zoals wat er in een zoekveld staat.
3. `replace` vervangt de url in de geschiedenis, zodat de terugknop niet door elke filter stapt.

Voor navigatie vanuit code, zoals een filter of een zoekveld, gebruik je de router in plaats van Link.

Meer in de docs: [Manual visits](https://inertiajs.com/docs/v3/the-basics/manual-visits)

> **Toelichting:** router.visit, router.post, router.delete en de rest bestaan ook. Een filter in de querystring heeft een voordeel: de url is te delen, en een refresh houdt de filter. In de controller lees je hem met $request->query('status'), zoals altijd, en je geeft hem terug als prop zodat het select-veld de juiste waarde toont.

<!-- slide 37 -->

### Testen

```php
use Inertia\Testing\AssertableInertia as Assert;

it('lists the projects', function () {
    Project::factory()->count(3)->create();

    $this->actingAs(User::factory()->create())
        ->get('/projects')
        ->assertInertia(fn (Assert $page) => $page
            ->component('projects/index')
            ->has('projects', 3));
});
```

Een feature test werkt zoals bij Blade: een request en een assert op de response. `assertInertia` checkt de component en de props, zonder dat React draait.

Meer in de docs: [Testing](https://inertiajs.com/docs/v3/advanced/testing)

> **Toelichting:** Zonder etc() faalt een scoped assert als er een prop bijkomt die je niet checkt. Dat is net handig: zo merk je het als er stil een kolom te veel naar de browser gaat. Meer over feature tests in de les over testen.

<!-- slide 38 -->

### Je gebruikt een gewone <a href="/projects/12"> in een Inertia-pagina. Wat gebeurt er?

- A. Een fout in de console
- B. Een volledige page reload: Laravel stuurt HTML en React start opnieuw
- C. Hetzelfde als met Link
- D. Niets, Inertia negeert de klik

**Antwoord:** B. Een volledige page reload: Laravel stuurt HTML en React start opnieuw

Een gewone link doet wat een link altijd doet: de browser vraagt een nieuwe pagina, zonder X-Inertia. Laravel stuurt dus de volledige HTML, en je app start opnieuw. Het werkt, maar je verliest de state van je layout, en het voelt trager. Link vangt de klik op en vraagt enkel de JSON.

> **Toelichting:** Dat maakt het makkelijk om een Blade-app stap voor stap om te zetten: Blade-pagina's en Inertia-pagina's kunnen naast elkaar bestaan, met gewone links ertussen.

<!-- slide 39 -->

## 6. Formulieren

> **Toelichting:** Hier wint Inertia het duidelijkst van een losse frontend. Je valideert zoals in Blade, je redirect zoals in Blade, en de fouten staan vanzelf bij je velden. Geen 422 om op te vangen, geen JSON om om te zetten.

<!-- slide 40 -->

### De controller

```php
public function store(StoreProjectRequest $request)
{
    $project = Project::create($request->validated());

    Inertia::flash('success', 'Project aangemaakt');

    return to_route('projects.show', $project);
}
```

Exact wat je in een Blade-app schrijft. Faalt de validatie, dan stuurt Laravel de browser terug naar het formulier, en de middleware zet de fouten in de prop `errors`. Lukt het, dan volgt Inertia de redirect en toont het de nieuwe pagina.

Meer in de docs: [Validation](https://inertiajs.com/docs/v3/the-basics/validation)

> **Toelichting:** Bij PUT, PATCH en DELETE zet de middleware een redirect om naar 303, zodat de browser de volgende pagina met GET ophaalt. Dat doet ze vanzelf, nog een reden om de middleware niet te vergeten. to_route() is een helper voor redirect()->route().

<!-- slide 41 -->

### Het Form-component

```tsx
<Form action={store()} resetOnSuccess>
  {({ errors, processing }) => (
    <>
      <label htmlFor="name">Naam</label>
      <input id="name" name="name" />
      {errors.name && <p>{errors.name}</p>}

      <button disabled={processing}>Opslaan</button>
    </>
  )}
</Form>
```

1. `Form` stuurt het formulier via Inertia, zonder page reload. `store()` uit Wayfinder geeft de url en de methode.
2. Een functie als child krijgt de staat van het formulier: fouten, `processing`, `wasSuccessful`.
3. Het veld heeft enkel een `name`, zoals in gewone HTML. De fout staat onder dezelfde sleutel als in je Form Request.
4. `processing` is true zolang het request loopt. Zo klikt niemand twee keer.

> **Toelichting:** Form heeft nog meer props: transform om de data aan te passen voor je ze stuurt, disableWhileProcessing, en errorBag voor twee formulieren op één pagina. resetOnSuccess maakt de velden leeg na een gelukte submit, handig voor een formulier dat op dezelfde pagina blijft.

<!-- slide 42 -->

### useForm

```tsx
const { data, setData, submit, processing, errors, progress } = useForm({
  name: '',
  cover: null as File | null,
})

<form onSubmit={(e) => { e.preventDefault(); submit(store()) }}>
  <input value={data.name} onChange={(e) => setData('name', e.target.value)} />
  …
```

Met `useForm` zit de data in React-state. Kies het als een veld afhangt van een ander, als je de data eerst wil omzetten, of voor een upload met een voortgangsbalk uit `progress`. Voor een gewoon formulier is `Form` korter.

Meer in de docs: [Forms](https://inertiajs.com/docs/v3/the-basics/forms)

> **Toelichting:** Een bestand in useForm stuurt Inertia vanzelf als multipart/form-data. Een upload met PUT werkt niet in PHP: stuur een POST met _method: 'put' in de data, zoals @method('PUT') in een Blade-formulier.

<!-- slide 43 -->

### Flash messages

*Controller*

```php
Inertia::flash('success', 'Project aangemaakt');
```

*components/toast.tsx*

```tsx
import { usePage } from '@inertiajs/react'

export function Toast() {
  const { flash } = usePage()

  return flash.success
    ? <div role="status">{flash.success}</div>
    : null
}
```

Flash data leeft één request, en komt niet in de geschiedenis van de browser. Wie terugklikt, ziet de melding niet opnieuw. Zet de toast in je layout, dan werkt hij op elke pagina.

Meer in de docs: [Flash data](https://inertiajs.com/docs/v3/data-props/flash-data)

> **Toelichting:** session()->flash() en ->with('success', ...) op een redirect werken ook, maar dan moet je de waarde zelf delen in HandleInertiaRequests. Inertia::flash() doet dat voor je. Wil je een melding na een request dat niet redirect, zoals met useHttp, dan heb je ze niet nodig: daar krijg je het antwoord zelf terug.

<!-- slide 44 -->

### De validatie faalt bij een Inertia-formulier. Wat krijgt de browser?

- A. Een 422 met de fouten als JSON, die je zelf verwerkt
- B. Een redirect terug, en dezelfde pagina met de fouten in de prop errors
- C. Een volledige page reload met een foutpagina
- D. Niets, de fouten staan enkel in de log

**Antwoord:** B. Een redirect terug, en dezelfde pagina met de fouten in de prop errors

Laravel doet wat het bij Blade ook doet: terug naar de vorige pagina, met de fouten in de sessie. De middleware van Inertia deelt ze als de prop errors, en Form en useForm zetten ze bij het juiste veld. Een 422 met JSON is wat een API teruggeeft, en daar schrijf je de code om ze te tonen zelf.

> **Toelichting:** Heb je twee formulieren op één pagina met dezelfde veldnamen, zoals twee keer email, geef ze dan elk een errorBag. Dan komen de fouten van elk formulier onder een eigen sleutel.

<!-- slide 45 -->

## 7. Gedeelde en trage data

> **Toelichting:** Sommige data heb je op elke pagina nodig, zoals de ingelogde gebruiker. Andere data is traag, zoals statistieken over een jaar orders. Inertia heeft voor allebei een oplossing, en ze zit in je controller of je middleware, niet in React.

<!-- slide 46 -->

### Gedeelde data

*HandleInertiaRequests*

```php
public function share(Request $request): array
{
    return [
        ...parent::share($request),
        'auth' => [
            'user' => $request->user()?->only('id', 'name'),
        ],
        'unread' => fn () => $request->user()
            ?->unreadNotifications()->count(),
    ];
}
```

*In een component*

```tsx
const { auth } = usePage().props

<p>Hallo {auth.user.name}</p>
```

Wat `share()` teruggeeft, komt bij de props van elke pagina. Een closure voert Laravel pas uit wanneer de prop in het antwoord moet, en bij een partial reload zonder die prop slaat hij ze over.

> **Toelichting:** Hou share() klein. Alles erin gaat mee met elk request, ook bij een klik op een filter. De starter kit deelt onder meer de naam van de app en de ingelogde user. Een volledige lijst rechten hoort er niet in, zie de les over autorisatie.

<!-- slide 47 -->

### Partial reloads

*OrderController*

```php
return Inertia::render('orders/index', [
    'orders' => fn () => Order::latest()->paginate(),
    'stats' => fn () => Order::statsForYear(),
]);
```

*pages/orders/index.tsx*

```tsx
router.reload({ only: ['orders'] })
```

`router.reload` haalt dezelfde pagina opnieuw, maar vraagt enkel de props in `only`. Omdat `stats` een closure is, voert Laravel die query dan niet uit. Handig voor een lijst die je ververst na een actie, of die je om de dertig seconden opnieuw ophaalt.

Meer in de docs: [Partial reloads](https://inertiajs.com/docs/v3/data-props/partial-reloads)

> **Toelichting:** Inertia::optional(fn () => ...) gaat een stap verder: die prop komt nooit mee, enkel als je hem met only vraagt. Inertia::always() doet het omgekeerde: die prop komt altijd mee, ook als hij niet in only staat. De les over realtime toont polling met router.reload, en het alternatief met WebSockets.

<!-- slide 48 -->

### Deferred props

*DashboardController*

```php
return Inertia::render('dashboard', [
    'projects' => $projects,
    'revenue' => Inertia::defer(
        fn () => Invoice::revenuePerMonth()
    ),
]);
```

*pages/dashboard.tsx*

```tsx
import { Deferred } from '@inertiajs/react'

<Deferred data="revenue" fallback={<Skeleton />}>
  <RevenueChart revenue={revenue} />
</Deferred>
```

De pagina komt meteen, zonder `revenue`. Daarna vraagt Inertia die prop vanzelf in een tweede request, en `Deferred` toont tot dan de fallback. De trage query houdt de rest van je pagina niet meer tegen.

Meer in de docs: [Deferred props](https://inertiajs.com/docs/v3/data-props/deferred-props)

> **Toelichting:** Zonder groep komen alle deferred props samen in één request. Met een tweede argument, Inertia::defer(fn () => ..., 'charts'), maak je groepen die elk een eigen request krijgen, zodat een trage grafiek een snelle teller niet ophoudt.

<!-- slide 49 -->

### useHttp

```tsx
import { useHttp } from '@inertiajs/react'

const [results, setResults] = useState<Client[]>([])
const { data, setData, get, processing } = useHttp({ query: '' })

function search(query: string) {
  setData('query', query)
  get('/clients/search', { onSuccess: (clients) => setResults(clients) })
}
```

Niet elk request is een pagina. Een autocomplete of een knop die iets aanzet, vraagt JSON zonder dat de url verandert. `useHttp`, nieuw in Inertia 3, doet dat op dezelfde manier als `useForm`. De route geeft gewone JSON terug, geen `Inertia::render`.

Meer in de docs: [HTTP requests](https://inertiajs.com/docs/v3/the-basics/http-requests)

> **Toelichting:** Vroeger gebruikte je daarvoor axios of fetch, en axios zat in elke Inertia-app. Inertia 3 heeft een eigen HTTP-client en laat axios vallen. Gebruik je axios nog in je eigen code, installeer het dan zelf. Zo'n JSON-route mag gewoon in routes/web.php staan, met dezelfde sessie als je pagina's.

<!-- slide 50 -->

### Optimistic updates

```tsx
router
  .optimistic((props) => ({
    project: { ...props.project, starred: !props.project.starred },
  }))
  .post(`/projects/${project.id}/star`)
```

De ster gaat meteen aan, nog voor de server antwoordt. Mislukt het request, dan zet Inertia de props terug. Gebruik het voor kleine dingen die bijna altijd lukken: een like, een vinkje, een nieuwe volgorde.

Meer in de docs: [Optimistic updates](https://inertiajs.com/docs/v3/the-basics/optimistic-updates)

> **Toelichting:** Bij een betaling of iets dat mails verstuurt doe je dit niet: daar wil de gebruiker zeker weten dat het gelukt is. Optimistic updates zijn nieuw in Inertia 3, en werken ook in useForm, Form en useHttp.

<!-- slide 51 -->

### SSR

Zonder SSR krijgt een crawler een lege div en een blok JSON. Voor een dashboard achter een login maakt dat niet uit, voor publieke pagina's die gevonden moeten worden wel.

Met SSR rendert een Node-proces je component op de server. In `npm run dev` doet de Vite-plugin dat zelf, in productie draait Node naast PHP.

```console
$ npm run build:ssr
vite build && vite build --ssr
$ php artisan inertia:start-ssr
Starting SSR server...
```

Meer in de docs: [Server-side rendering](https://inertiajs.com/docs/v3/advanced/server-side-rendering)

> **Toelichting:** Een tussenweg die vaak beter werkt: de publieke pagina's in Blade of Kirby, de app achter de login in Inertia, zonder SSR. Dan heb je geen Node op je server nodig. Laravel Cloud en Forge kunnen het SSR-proces voor je draaien als je het toch nodig hebt.

<!-- slide 52 -->

### Een dashboard laadt traag omdat één grafiek een query van drie seconden doet. Wat helpt het meest?

- A. De query in share() zetten
- B. De prop uitstellen met Inertia::defer()
- C. Een gewone a-tag gebruiken in plaats van Link
- D. SSR aanzetten

**Antwoord:** B. De prop uitstellen met Inertia::defer()

Met defer komt de pagina meteen, en de grafiek in een tweede request. share() maakt het erger: dan draait de query op elke pagina. SSR verandert waar de HTML gemaakt wordt, niet hoe lang je query duurt.

> **Toelichting:** Drie seconden blijft drie seconden. Kijk ook naar de query zelf: een index, eager loading of een cache. De les over Eloquent queries gaat daar dieper op in.

<!-- slide 53 -->

## 8. Wanneer wat

> **Toelichting:** Er is geen beste aanpak. Er is een aanpak die past bij de klant, het team en wat er later nog bij komt. In The Agency maak je die keuze bij de start van een project, en verander je ze liefst niet halverwege.

<!-- slide 54 -->

### En Livewire

- **Inertia**: Componenten in React. De server stuurt data, de browser maakt de HTML. Je hebt het hele ecosysteem van React.
- **Livewire**: Componenten in PHP en Blade. Elke interactie is een request naar de server, die nieuwe HTML terugstuurt.

Livewire is de vierde optie, en er is ook een starter kit voor. Het past bij een team dat vooral PHP schrijft, en bij interactie die beperkt blijft tot formulieren, filters en modals. Heb je een grafiekenlibrary, drag and drop of een rijke editor nodig, dan zit je bij React beter.

Meer in de docs: [Livewire](https://livewire.laravel.com/docs)

> **Toelichting:** Livewire 4 zit in de Livewire-starter kit, met Flux als componentenbibliotheek. Elke klik is een roundtrip naar de server: op een trage verbinding voel je dat. Bij Inertia gebeurt een deel van de interactie in de browser zonder request, zoals een dropdown of een tab.

<!-- slide 55 -->

### Kiezen

| Project | Keuze | Waarom |
| --- | --- | --- |
| Campagnesite met een redactie | Kirby of Blade | Vooral content, en Google moet het lezen |
| Dashboard of portaal voor de klant | Inertia | Veel interactie, één client, login met sessies |
| Webapp en een mobiele app | Inertia en een API | De app krijgt `routes/api.php`, het web blijft Inertia |
| Een frontend-team met Next.js | Een API | Twee teams, twee projecten, een contract ertussen |
| Formulieren en tabellen, het team schrijft PHP | Livewire | Geen JavaScript-framework om te leren |

> **Toelichting:** Het derde scenario wordt het vaakst verkeerd ingeschat: een mobiele app betekent niet dat je web ook een losse frontend moet worden. Je bouwt de API voor de app erbij, en de logica zit in actions die zowel de Inertia-controller als de API-controller oproepen. Zie de lessen over architectuur en API's.

<!-- slide 56 -->

### Inertia en een API samen

```php
// Web\ProjectController::store()
$project = $create->handle($request->validated());

return to_route('projects.show', $project);

// Api\ProjectController::store()
$project = $create->handle($request->validated());

return ProjectResource::make($project);
```

- Twee controllers, één action met de logica. Beide krijgen de action en de Form Request binnen als parameter.
- De web-controller redirect, of geeft Inertia::render terug.
- De API-controller geeft een resource terug, met 201.

> **Toelichting:** Dat is de aanpak uit de les over architectuur: de controller vertaalt HTTP naar een aanroep van je action, en terug. Login verschilt wel: de web-routes gebruiken de sessie, de app gebruikt tokens van Sanctum.

<!-- slide 57 -->

### Valkuilen

- Een volledig model als prop, met kolommen die de browser niet mag zien.
- Een gewone a-tag tussen je pagina's, en dus een volledige reload bij elke klik.
- Zware queries in share(), die daardoor op elke pagina draaien.
- Een redirect naar een andere site met redirect()->away() in plaats van Inertia::location().
- Toch een API bouwen voor je eigen Inertia-frontend, en de helft van Inertia niet gebruiken.

> **Toelichting:** De laatste zie je vaak bij wie van een losse React-app komt: een Inertia-pagina die in een useEffect zijn data ophaalt bij /api/projects. Dan heb je de nadelen van allebei. Geef de data mee als prop, en gebruik useHttp enkel voor wat echt geen pagina is.

<!-- slide 58 -->

### Een sportclub wil een reservatiesysteem in de browser, en volgend jaar misschien een app. Wat kies je nu?

- A. Een losse React-app met een API, voor als de app komt
- B. Inertia, en een API erbij als de app er echt komt
- C. Blade, want de app is nog niet zeker
- D. Kirby

**Antwoord:** B. Inertia, en een API erbij als de app er echt komt

Bouw voor wat je nu weet. Inertia geeft een interactieve webapp zonder API, en komt de app er, dan voeg je routes/api.php toe die dezelfde actions oproept. Een losse frontend voor een app die misschien komt, betekent een jaar lang de kosten van twee projecten. Blade kan, maar een reservatiekalender is net het soort interactie waar React helpt. Kirby is gemaakt voor content, niet voor reservaties.

> **Toelichting:** Schrijf zo'n keuze en de reden erbij op in de README of de documentatie van je project. Wie het project later overneemt, weet dan waarom er geen API is, en dat dat geen vergetelheid was.

<!-- slide 59 -->

### Opdracht: Projecten in Inertia

Bouw in een nieuw project met de React starter kit een lijst met projecten, een detailpagina en een formulier.

1. Een model Project met name, client en deadline, met een factory en een seeder
2. Een index- en een show-pagina met Inertia::render, props met only() en links met Wayfinder
3. Een create-pagina met Form, een Form Request en de fout onder elk veld
4. Een flash message na het aanmaken, getoond in de layout, en een filter op klant met router.get
5. Klaar als je van de lijst naar een nieuw project en terug kan zonder één volledige page reload, en de network tab enkel JSON toont

> **Toelichting:** Begin met php artisan make:model Project -mfsc en de routes, en schrijf de controller voor de pagina: dan weet je welke props je component krijgt. Vast bij de fouten? Kijk terug naar Het Form-component, en controleer of de name van je input dezelfde is als de sleutel in je Form Request. Zie je een modal met HTML erin, dan gaf een request geen Inertia-response terug: kijk in storage/logs/laravel.log.

## Begrippen

- **Inertia**: Een protocol met adapters voor Laravel en voor React, Vue of Svelte. Je controller geeft een component en props terug, en de browser rendert die zonder dat je een API bouwt. (slide 15)
- **Single-page application**: Een webapp die de browser één keer laadt, en daarna enkel de inhoud wisselt met JavaScript. Afgekort SPA. Een Inertia-app voelt zo voor de gebruiker. (slide 10)
- **Losse frontend**: Een frontend in een eigen project, zoals een React-app, die al zijn data via een API uit een aparte backend haalt. Ook decoupled frontend genoemd. (slide 7)
- **Page component**: Een React-component in resources/js/pages die Inertia rendert voor een route. De naam in Inertia::render() is het pad naar dat bestand. (slide 17)
- **Props**: De array die je controller aan Inertia::render() meegeeft. Ze komen als JSON in de browser, en zijn dus voor iedereen leesbaar. (slide 16)
- **Page object**: De JSON die Inertia bij elke pagina meestuurt: component, props, url en version. Bij het eerste bezoek zit hij in de HTML, daarna komt hij alleen. (slide 21)
- **X-Inertia**: De header waarmee de client laat weten dat hij een page object als JSON wil in plaats van een volledige HTML-pagina. (slide 22)
- **Asset versioning**: Inertia vergelijkt de versie van je JavaScript in de browser met die op de server. Verschillen ze na een deploy, dan antwoordt Laravel met 409 en laadt de browser de pagina volledig opnieuw. (slide 23)
- **Root template**: De enige Blade-view in een Inertia-app, resources/views/app.blade.php. Ze laadt je bundle met @vite en zet de page object op de pagina met @inertia. (slide 30)
- **HandleInertiaRequests**: De middleware van Inertia in de web-groep. Ze deelt data met elke pagina, zet validatiefouten in de props en controleert de versie van je assets. (slide 28)
- **Persistente layout**: Een layout die blijft staan als je naar een andere pagina gaat, met Page.layout. Enkel de pagina erin wisselt, dus de state van de layout blijft. (slide 33)
- **Wayfinder**: Een package van Laravel dat van je routes en controller-methodes TypeScript-functies maakt. Een route die niet meer bestaat, breekt de build in plaats van een 404 te geven. (slide 35)
- **Flash data**: Data die één request leeft, zoals een melding na het opslaan. Met Inertia::flash() komt ze in usePage().flash, en niet in de geschiedenis van de browser. (slide 43)
- **Shared data**: Props die share() in HandleInertiaRequests aan elke pagina toevoegt, zoals de ingelogde gebruiker. (slide 46)
- **Partial reload**: Dezelfde pagina opnieuw ophalen, maar enkel de props die je in only vraagt. Props die als closure meegegeven zijn, voert Laravel dan niet uit. (slide 47)
- **Deferred props**: Props die Inertia na de eerste render in een tweede request ophaalt, met Inertia::defer(). Zo houdt een trage query de rest van de pagina niet tegen. (slide 48)
- **useHttp**: Een hook uit Inertia 3 voor een request dat geen pagina is, zoals een autocomplete. De url en de pagina blijven staan. (slide 49)
- **Optimistic update**: De UI meteen aanpassen alsof het request al gelukt is. Mislukt het, dan zet Inertia de props terug. (slide 50)
- **SSR**: Server-side rendering: een Node-proces rendert je React-component op de server, zodat de browser en Google meteen HTML krijgen. (slide 51)
- **Livewire**: Een alternatief voor Inertia met componenten in PHP en Blade. Elke interactie is een request naar de server, die nieuwe HTML terugstuurt. (slide 54)

## Links en bronnen

### In de presentatie

- [Sanctum SPA authentication](https://laravel.com/docs/13.x/sanctum#spa-authentication) (slide 7)
- [How it works](https://inertiajs.com/docs/v3/core-concepts/how-it-works) (slide 16)
- [The protocol](https://inertiajs.com/docs/v3/core-concepts/the-protocol) (slide 22): De page object, X-Inertia en de 409 bij een nieuwe versie
- [Asset versioning](https://inertiajs.com/docs/v3/advanced/asset-versioning) (slide 23)
- [Starter kits](https://laravel.com/docs/13.x/starter-kits#react) (slide 26)
- [Server-side setup](https://inertiajs.com/docs/v3/installation/server-side-setup) (slide 28)
- [Client-side setup](https://inertiajs.com/docs/v3/installation/client-side-setup) (slide 29)
- [Layouts](https://inertiajs.com/docs/v3/the-basics/layouts) (slide 33)
- [Links](https://inertiajs.com/docs/v3/the-basics/links) (slide 34)
- [Laravel Wayfinder](https://github.com/laravel/wayfinder) (slide 35): Nog in beta
- [Manual visits](https://inertiajs.com/docs/v3/the-basics/manual-visits) (slide 36)
- [Testing](https://inertiajs.com/docs/v3/advanced/testing) (slide 37)
- [Validation](https://inertiajs.com/docs/v3/the-basics/validation) (slide 40)
- [Forms](https://inertiajs.com/docs/v3/the-basics/forms) (slide 42): Form en useForm
- [Flash data](https://inertiajs.com/docs/v3/data-props/flash-data) (slide 43)
- [Partial reloads](https://inertiajs.com/docs/v3/data-props/partial-reloads) (slide 47)
- [Deferred props](https://inertiajs.com/docs/v3/data-props/deferred-props) (slide 48)
- [HTTP requests](https://inertiajs.com/docs/v3/the-basics/http-requests) (slide 49)
- [Optimistic updates](https://inertiajs.com/docs/v3/the-basics/optimistic-updates) (slide 50)
- [Server-side rendering](https://inertiajs.com/docs/v3/advanced/server-side-rendering) (slide 51)
- [Livewire](https://livewire.laravel.com/docs) (slide 54)

### Bronnen

- [Inertia documentation](https://inertiajs.com/docs/v3): Versie 3
- [Upgrade guide for v3.0](https://inertiajs.com/docs/v3/getting-started/upgrade-guide): useHttp, optimistic updates, de Vite-plugin, axios eruit
- [Laravel starter kits](https://laravel.com/docs/13.x/starter-kits): De React starter kit met Inertia 3, shadcn/ui en Fortify
