Zum Hauptinhalt springen

Was ist React?

React ist eine von Meta (früher Facebook) entwickelte und gepflegte JavaScript-Bibliothek zum Aufbau von Benutzeroberflächen. Kennzeichnend sind die deklarative UI-Beschreibung und die komponentenbasierte Architektur; React wird für kleine Widgets bis hin zu vollständigen SPAs eingesetzt. Der Kern von React ist das effiziente Re-Rendering über einen virtuellen DOM. Ändert sich der State, überträgt React nur die Differenz ins DOM – Sie müssen das DOM nicht manuell manipulieren.
Diese Seite behandelt die Kombination React 19 mit Inertia v3. Die Starterkits von Laravel 13 verwenden diese Konstellation standardmäßig.

JSX und TSX

React-Komponenten werden in JSX (JavaScript XML) geschrieben. Eine HTML-ähnliche Syntax lässt sich direkt in JavaScript einbetten.
// JSX-Beispiel
function Greeting({ name }) {
    return <h1>Hallo, {name}</h1>
}
In den Starterkits ist TypeScript (.tsx) Standard. Typdefinitionen verbessern die IDE-Vervollständigung und helfen, Bugs früh zu erkennen.
// TSX-Beispiel (TypeScript)
type Props = {
    name: string
}

function Greeting({ name }: Props) {
    return <h1>Hallo, {name}</h1>
}
Das React-Starterkit von Laravel setzt standardmäßig auf TypeScript + TSX. Alle Beispiele auf dieser Seite sind ebenfalls in TSX geschrieben.

Rolle in Laravel

Historie

Die Beziehung zwischen React und Laravel ist etwas jünger als die zu Vue, mittlerweile aber mindestens gleichrangig. Mit Laravel 6 (2019) wurde das Auth-Scaffold in das Paket laravel/ui ausgelagert; zusammen mit Vue erschien auch ein React-Scaffold. Zu diesem Zeitpunkt dominierte Vue, und die React-Variante fristete ein Nischendasein. In Laravel Breeze (2021) wurde der Inertia + React-Stack ergänzt und damit die ernsthafte Adoption gestartet. Mit der Starterkit-Erneuerung in Laravel 12 (2025) wurde React vollständig gleichrangig zu Vue (und wird sogar zuerst angezeigt).

Aktueller Standard: Inertia × React

Der Standard-Ansatz für React in Laravel ist heute Inertia × React. Inertia ermöglicht die „moderne Monolith”-Architektur, bei der Sie ohne API-Design direkt aus dem Laravel-Controller Daten an React-Komponenten übergeben.

Setup

Via Starterkit (empfohlen)

Beim Aufsetzen eines neuen Projekts ist das Starterkit am komfortabelsten.
laravel new my-app
Wenn Sie im interaktiven Prompt React wählen, wird automatisch alles Folgende eingerichtet:
  • inertiajs/inertia-laravel (serverseitiger Adapter)
  • @inertiajs/react (Client-Adapter)
  • react + react-dom (React 19 selbst)
  • @vitejs/plugin-react (Vite-Plugin)
  • TypeScript + @types/react
  • Tailwind CSS + shadcn/ui-Komponenten
  • Middleware HandleInertiaRequests
  • Auth-Seiten für Login, Registrierung u. a. (bereits mit Inertia + React + TypeScript umgesetzt)

Manuelle Installation

Für eine Ergänzung in einem bestehenden Projekt installieren Sie Server- und Client-Seite separat.
# Serverseitig (PHP)
composer require inertiajs/inertia-laravel

# Clientseitig (JavaScript)
npm install @inertiajs/react react react-dom
npm install --save-dev @vitejs/plugin-react @types/react @types/react-dom typescript
Ergänzen Sie anschließend das React-Plugin in vite.config.ts:
import { defineConfig } from 'vite'
import laravel from 'laravel-vite-plugin'
import react from '@vitejs/plugin-react'

export default defineConfig({
    plugins: [
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.tsx'],
            refresh: true,
        }),
        react(),
    ],
})
Starten Sie die Inertia-App in resources/js/app.tsx:
import { createInertiaApp } from '@inertiajs/react'
import { createRoot } from 'react-dom/client'
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'

createInertiaApp({
    resolve: (name) =>
        resolvePageComponent(
            `./pages/${name}.tsx`,
            import.meta.glob('./pages/**/*.tsx'),
        ),
    setup({ el, App, props }) {
        createRoot(el).render(<App {...props} />)
    },
})
Details zur manuellen Installation (Root-Template, Middleware-Registrierung usw.) finden Sie in der offiziellen Inertia-Dokumentation.

Verzeichnisstruktur

Im Starterkit werden React-Page-Komponenten unter resources/js/pages/ abgelegt.
resources/js/
├── app.tsx            # Einstiegspunkt der Inertia-App
├── bootstrap.ts
├── components/        # Wiederverwendbare UI-Komponenten
│   ├── ui/            # shadcn/ui-Komponenten
│   └── ...
├── hooks/             # Benutzerdefinierte React-Hooks
├── layouts/           # Layout-Komponenten
│   ├── app-layout.tsx
│   └── auth-layout.tsx
├── lib/               # Utility-Funktionen und Konfiguration
├── pages/             # Inertia-Page-Komponenten (analog zu Controller-Namen)
│   ├── auth/
│   │   ├── login.tsx
│   │   └── register.tsx
│   ├── dashboard.tsx
│   └── posts/
│       ├── index.tsx
│       ├── create.tsx
│       └── show.tsx
└── types/             # TypeScript-Typdefinitionen
Inertia::render('posts/index', [...]) verweist auf die Komponente resources/js/pages/posts/index.tsx.

Grundlagen der JSX-Syntax

React verwendet JSX. Nachfolgend die minimal notwendigen Muster, um den Code der Starterkits lesen zu können.

{} – Variablen-Interpolation

Innerhalb von JSX betten Sie JavaScript-Werte und -Ausdrücke mit {} ein.
const name = 'Welt'
const count = 3

return (
    <div>
        <p>Hallo, {name}!</p>
        <p>Doppelter Wert: {count * 2}</p>
    </div>
)

Bedingte Darstellung – && und ternärer Operator

React hat keine Direktive wie v-if. Für einfache Bedingungen nutzen Sie den &&-Operator, für if/else den ternären Operator ? :.
{/* Nur anzeigen, wenn Bedingung true */}
{isLoggedIn && <p>Willkommen!</p>}

{/* if / else */}
{isLoggedIn ? <p>Willkommen!</p> : <a href="/login">Anmelden</a>}

{/* if / else if / else */}
{isLoggedIn
    ? <p>Willkommen!</p>
    : role === 'admin'
        ? <p>Als Administrator angemeldet</p>
        : <a href="/login">Anmelden</a>
}

Listen darstellen – .map()

Für Listen wird Array.map() verwendet. Für ein effizientes diffbasiertes Rendering ist das key-Prop erforderlich.
<ul>
    {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
    ))}
</ul>
Entspricht v-for :key in Vue oder {#each} in Svelte.

className – CSS-Klassennamen

Da JSX zu JavaScript kompiliert wird und class ein reserviertes Wort ist, verwenden Sie für CSS-Klassen className.
{/* HTML: <div class="container"> */}
<div className="container">...</div>

Event-Handler – camelCase

Event-Attribute in JSX werden in camelCase geschrieben; Sie übergeben eine Funktionsreferenz.
<button onClick={handleClick}>Klick</button>

{/* Default-Verhalten des Formular-Submits verhindern */}
<form onSubmit={(e) => { e.preventDefault(); handleSubmit() }}>
    ...
</form>

Grundlagen der Page-Komponente

Eine Inertia-Page-Komponente ist eine gewöhnliche React-Komponente. Die aus dem Laravel-Controller übergebenen Daten stehen als Props zur Verfügung.

Controller

// app/Http/Controllers/PostController.php
use Inertia\Inertia;
use App\Models\Post;

class PostController extends Controller
{
    public function index()
    {
        return Inertia::render('posts/index', [
            'posts' => Post::latest()->paginate(10),
        ]);
    }
}

React-Page-Komponente

// resources/js/pages/posts/index.tsx
import { Link } from '@inertiajs/react'

type Post = {
    id: number
    title: string
    created_at: string
}

type Props = {
    posts: {
        data: Post[]
        // paginate(10) liefert ein Objekt mit Pagination-Infos
        // current_page, last_page, per_page, total sind ebenfalls verfügbar
    }
}

export default function PostsIndex({ posts }: Props) {
    return (
        <div>
            <h1>Beitragsübersicht</h1>
            {posts.data.map((post) => (
                <article key={post.id}>
                    <h2>
                        <Link href={`/posts/${post.id}`}>{post.title}</Link>
                    </h2>
                    <p>{post.created_at}</p>
                </article>
            ))}
        </div>
    )
}
Sie empfangen die Props als Argument der Komponente und verwenden die vom Controller übergebenen Daten direkt. Eine REST-API muss nicht definiert werden.
Die von @inertiajs/react bereitgestellte <Link>-Komponente sorgt dafür, dass Seitenwechsel per XHR ablaufen und ein Full-Page-Reload vermieden wird.
import { Link } from '@inertiajs/react'

export default function PostsIndex() {
    return (
        <div>
            {/* Einfacher Link */}
            <Link href="/posts">Beitragsübersicht</Link>

            {/* Link mit POST-Methode (z. B. Löschen) */}
            <Link href="/posts/1" method="delete" as="button">
                Löschen
            </Link>

            {/* Preload (bei Hover vorab laden) */}
            <Link href="/posts/1" preload>
                Beitrag ansehen
            </Link>
        </div>
    )
}
Sie schreiben wie eine gewöhnliche <a>-Anweisung. Im Hintergrund tauscht Inertia nur die Page-Komponente aus – so entsteht das SPA-artige Gefühl.

Die Form-Komponente

Die <Form>-Komponente von @inertiajs/react ist der empfohlene Stil zum Absenden von Formularen im Starterkit. Sie geben action und method als Props an und erhalten errors und processing über eine Children-Funktion (Render-Prop).

Grundlegende Verwendung

import { Form } from '@inertiajs/react'

export default function PostCreate() {
    return (
        <Form action="/posts" method="post" className="flex flex-col gap-4">
            {({ errors, processing }) => (
                <>
                    <div>
                        <label htmlFor="title">Titel</label>
                        <input id="title" name="title" type="text" required />
                        {errors.title && <p className="error">{errors.title}</p>}
                    </div>

                    <div>
                        <label htmlFor="content">Inhalt</label>
                        <textarea id="content" name="content" />
                        {errors.content && <p className="error">{errors.content}</p>}
                    </div>

                    <button type="submit" disabled={processing}>
                        {processing ? 'Wird gesendet...' : 'Veröffentlichen'}
                    </button>
                </>
            )}
        </Form>
    )
}
Das Children von <Form> ist eine Funktion ({ errors, processing }) => JSX (Render-Prop-Muster). Die Form-Komponente berechnet diese Werte automatisch und übergibt sie. Formularfelder verwenden statt eines onChange-Handlers das HTML-native name-Attribut, sodass die eingebaute Formularerfassung des Browsers funktioniert.

Muster aus dem Starterkit

Die Starterkits verwalten Routen mit Wayfinder als Objekte. store.form() gibt ein Objekt mit action und method aus dem Route-Objekt zurück, das per Spread an <Form> übergeben wird.
import { Form } from '@inertiajs/react'
import { store } from '@/routes/login'

export default function Login() {
    return (
        <Form
            {...store.form()}
            resetOnSuccess={['password']}
            className="flex flex-col gap-6"
        >
            {({ errors, processing }) => (
                <>
                    {/* Formularinhalt */}
                </>
            )}
        </Form>
    )
}
Die in resetOnSuccess angegebenen Felder werden bei erfolgreichem Absenden automatisch zurückgesetzt. Nutzen Sie das etwa für Passwortfelder, die nach dem Absenden geleert werden sollen.
Ohne Wayfinder verhalten sich action="/login" und Co. identisch, sobald Sie die URL direkt übergeben.

Der useForm-Hook

Für die Formularverarbeitung nutzen Sie den useForm-Hook aus @inertiajs/react. State-Handling, Absenden und Anzeige von Validierungsfehlern lassen sich damit schlank umsetzen.

Auf der Controller-Seite

// app/Http/Controllers/PostController.php
class PostController extends Controller
{
    public function store(Request $request)
    {
        $validated = $request->validate([
            'title'   => ['required', 'string', 'max:255'],
            'content' => ['required', 'string'],
        ]);

        Post::create($validated + ['user_id' => auth()->id()]);

        return redirect()->route('posts.index')
            ->with('success', 'Der Beitrag wurde erstellt.');
    }
}

React-Formular-Komponente

// resources/js/pages/posts/create.tsx
import { useForm } from '@inertiajs/react'
import { FormEventHandler } from 'react'

export default function PostCreate() {
    const { data, setData, post, processing, errors } = useForm({
        title: '',
        content: '',
    })

    const submit: FormEventHandler = (e) => {
        e.preventDefault()
        post('/posts')
    }

    return (
        <form onSubmit={submit}>
            <div>
                <label>Titel</label>
                <input
                    type="text"
                    value={data.title}
                    onChange={(e) => setData('title', e.target.value)}
                />
                {errors.title && <p className="error">{errors.title}</p>}
            </div>

            <div>
                <label>Inhalt</label>
                <textarea
                    value={data.content}
                    onChange={(e) => setData('content', e.target.value)}
                />
                {errors.content && <p className="error">{errors.content}</p>}
            </div>

            <button type="submit" disabled={processing}>
                {processing ? 'Wird gesendet...' : 'Veröffentlichen'}
            </button>
        </form>
    )
}
Wichtige Eigenschaften des von useForm zurückgegebenen Objekts:
Eigenschaft / MethodeBeschreibung
dataDatenobjekt des Formulars
setData(field, value)Wert eines Feldes aktualisieren
errorsValidierungsfehler (Zugriff per Feldname)
processingtrue, solange das Formular gesendet wird (für Button-Deaktivierung)
isDirtytrue, wenn sich der Wert vom Ursprung unterscheidet
post(url)Per POST senden
put(url)Per PUT senden (Update)
delete(url)Per DELETE senden
reset()Formular auf Initialwerte zurücksetzen
Bei Validierungsfehlern behält useForm die Eingaben und zeigt die Fehler an.

Shared Data

Daten, die auf allen Seiten gemeinsam nötig sind (angemeldeter Nutzer, Flash-Messages u. a.), definieren Sie in der Methode share() der Middleware HandleInertiaRequests.
// app/Http/Middleware/HandleInertiaRequests.php
use Illuminate\Http\Request;
use Inertia\Middleware;

class HandleInertiaRequests extends Middleware
{
    public function share(Request $request): array
    {
        return array_merge(parent::share($request), [
            'auth' => [
                'user' => $request->user()
                    ? $request->user()->only('id', 'name', 'email')
                    : null,
            ],
            'flash' => [
                'success' => $request->session()->get('success'),
                'error'   => $request->session()->get('error'),
            ],
        ]);
    }
}
Für den Zugriff aus einer React-Komponente verwenden Sie den usePage()-Hook.
import { usePage } from '@inertiajs/react'

type SharedProps = {
    auth: {
        user: { id: number; name: string; email: string } | null
    }
    flash: {
        success: string | null
        error: string | null
    }
}

export default function AppHeader() {
    const { auth, flash } = usePage<SharedProps>().props

    return (
        <>
            <header>
                {auth.user ? (
                    <span>{auth.user.name}</span>
                ) : (
                    <span>Gast</span>
                )}
            </header>

            {flash.success && (
                <div className="alert-success">{flash.success}</div>
            )}
        </>
    )
}
Da Shared Data in jeder Anfrage enthalten sind, sollten Sie sie auf das Nötigste beschränken. Eine Lazy-Evaluation per fn() wird nur ausgeführt, wenn die Daten tatsächlich gelesen werden.

Grundlagen der React-Hooks

Wichtige React-Hooks für die Arbeit mit Inertia × React:

useState – lokaler State

import { useState } from 'react'

export default function Counter() {
    const [count, setCount] = useState(0)
    const [isOpen, setIsOpen] = useState(false)

    return (
        <div>
            <p>{count}</p>
            <button onClick={() => setCount(count + 1)}>+1</button>
            <button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
        </div>
    )
}

useEffect – Seiteneffekte

import { useState, useEffect } from 'react'

export default function Timer() {
    const [seconds, setSeconds] = useState(0)

    useEffect(() => {
        const timer = setInterval(() => {
            setSeconds((s) => s + 1)
        }, 1000)

        // Cleanup-Funktion
        return () => clearInterval(timer)
    }, []) // Leeres Array = nur einmal beim Mount
    return <p>Verstrichene Zeit: {seconds} s</p>
}

useMemo und useCallback – Performance-Optimierung

import { useMemo, useCallback } from 'react'
import { router } from '@inertiajs/react'

type Post = { id: number; title: string; published: boolean }
type Props = { posts: Post[] }

export default function PostsList({ posts }: Props) {
    // Wert memoisieren (nur neu berechnen, wenn sich posts ändern)
    const publishedPosts = useMemo(
        () => posts.filter((post) => post.published),
        [posts],
    )

    // Funktion memoisieren (nur neu erzeugen, wenn sich Abhängigkeiten ändern)
    const handleClick = useCallback((id: number) => {
        router.visit(`/posts/${id}`)
    }, [])

    return (
        <ul>
            {publishedPosts.map((post) => (
                <li key={post.id} onClick={() => handleClick(post.id)}>
                    {post.title}
                </li>
            ))}
        </ul>
    )
}

TypeScript-Unterstützung

Die React-Variante der Starterkits nutzt standardmäßig TypeScript. In Kombination mit den Typdefinitionen von Inertia erhalten Sie Typsicherheit für Props.

Globale Typdefinitionen

In den Starterkits werden die Typen der Shared Data in resources/js/types/index.d.ts definiert.
// resources/js/types/index.d.ts
export interface User {
    id: number
    name: string
    email: string
    email_verified_at?: string
}

export type PageProps<T extends Record<string, unknown> = Record<string, unknown>> = T & {
    auth: {
        user: User
    }
}

Verwendung der Typen in Page-Komponenten

import { PageProps } from '@/types'

type Post = {
    id: number
    title: string
    content: string
}

export default function PostsIndex({ auth, posts }: PageProps<{ posts: Post[] }>) {
    return (
        <div>
            <p>Angemeldet: {auth.user.name}</p>
            {posts.map((post) => (
                <article key={post.id}>
                    <h2>{post.title}</h2>
                </article>
            ))}
        </div>
    )
}

Fazit

React entfaltet in Kombination mit Laravel – insbesondere über Inertia im Sinne eines „modernen Monolithen” – seine Stärken. Die Zusammenarbeit mit TypeScript ist ausgezeichnet, was React besonders für große Anwendungen prädestiniert.
BausteinRolle
Laravel-ControllerRouting, Datenerfassung, Validierung
Inertia::render()Daten vom Controller an die React-Komponente reichen
React-Page-KomponenteProps empfangen und UI rendern
useFormFormular-State, Absenden und Fehleranzeige
Link-KomponenteSeitenwechsel ohne Full-Reload
usePage().propsZugriff auf Shared Data
TypeScriptTypsicherheit der Props und bessere IDE-Autocompletion
Mit Inertia × React verbinden Sie die Einfachheit des Laravel-Backends mit dem starken Ökosystem von React. Legen Sie ein Projekt mit dem Starterkit an, sind auch die Auth-Screens sofort einsatzbereit.

Offizielle Inertia.js-Dokumentation

Alle Funktionen von Inertia v3 finden Sie in der offiziellen Dokumentation.
Zuletzt geändert am 13. Juli 2026