> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Getting started with React — the essentials for using it with Inertia × Laravel

> An introductory guide to using React with Laravel + Inertia.js. Covers the history from laravel/ui to the current starter kits, plus practical usage of Inertia React hooks like useForm and usePage.

## What is React?

React is a JavaScript library for building user interfaces, developed and maintained by Meta (formerly Facebook). It's known for declarative UI descriptions and a **component-based** architecture, and is used across everything from small widgets to full SPAs.

React's core idea is efficient re-rendering via a **virtual DOM**. When state changes, React applies only the diff to the DOM, so you don't have to manipulate the DOM manually.

<Info>
  This page describes the combination of React 19 and Inertia v3. The Laravel 13 starter kit uses this stack by default.
</Info>

### JSX and TSX

React components are written in **JSX** (JavaScript XML), a syntax that lets you embed HTML-like markup directly in JavaScript.

```jsx theme={null}
// JSX example
function Greeting({ name }) {
    return <h1>Hello, {name}</h1>
}
```

The starter kits adopt **TypeScript** (`.tsx`) as the standard. Type definitions enhance IDE completion and help catch bugs early.

```tsx theme={null}
// TSX example (TypeScript)
type Props = {
    name: string
}

function Greeting({ name }: Props) {
    return <h1>Hello, {name}</h1>
}
```

<Tip>
  Laravel's React starter kit uses TypeScript + TSX as the standard. All examples on this page are written in TSX.
</Tip>

***

## React's position in Laravel

### History

The relationship between React and Laravel is slightly younger than Vue's, but today they are on equal footing.

```mermaid theme={null}
timeline
    title Laravel and React history
    2019 : Laravel 6 — React scaffolding added to laravel/ui
    2021 : Laravel 8 — Inertia React stack added to Breeze
    2022 : Laravel 9 — migration to Vite
    2025 : Laravel 12 — starter kits revamped (React / Vue)
    2026 : Laravel 13 — starter kits with Inertia v3 support
```

**In Laravel 6 (2019)**, authentication scaffolding was split out into the `laravel/ui` package, which offered React scaffolding alongside Vue. At the time, Vue was mainstream and the React version had less visibility.

**Laravel Breeze (2021)** added the Inertia + React stack, marking the start of serious adoption. With the **Laravel 12 (2025)** starter kit revamp, React became fully equal to Vue (and is now shown first).

### The current mainstream style: Inertia × React

The center of gravity for React in Laravel today is **Inertia × React**. Inertia enables a "modern monolith" architecture where data flows from Laravel controllers directly to React components without designing an API.

```mermaid theme={null}
graph LR
    Browser["Browser"]
    Inertia["Inertia.js<br>(adapter layer)"]
    Laravel["Laravel<br>(controller)"]
    React["React<br>(page component)"]

    Browser <-->|XHR / full page load| Inertia
    Inertia <-->|Inertia response| Laravel
    Inertia -->|props| React
    React -->|render| Browser
```

***

## Setup

### Via starter kit (recommended)

For new projects, using a starter kit is the easiest option.

```shell theme={null}
laravel new my-app
```

Choosing **React** in the interactive prompt automatically sets up all of the following:

* `inertiajs/inertia-laravel` (server-side adapter)
* `@inertiajs/react` (client adapter)
* `react` + `react-dom` (React 19 itself)
* `@vitejs/plugin-react` (Vite plugin)
* TypeScript + `@types/react`
* Tailwind CSS + the shadcn/ui component library
* The `HandleInertiaRequests` middleware
* Auth screens like login and registration (already implemented in Inertia + React + TypeScript)

### Manual installation

To add it to an existing project, install the server-side and client-side pieces separately.

```shell theme={null}
# Server side (PHP)
composer require inertiajs/inertia-laravel

# Client side (JavaScript)
npm install @inertiajs/react react react-dom
npm install --save-dev @vitejs/plugin-react @types/react @types/react-dom typescript
```

Next, add the React plugin to `vite.config.ts`.

```ts theme={null}
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(),
    ],
})
```

Start the Inertia app from `resources/js/app.tsx`.

```tsx theme={null}
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} />)
    },
})
```

<Info>
  For manual installation details (root template configuration, middleware registration, etc.), see the [Inertia official docs](https://inertiajs.com/installation).
</Info>

***

## Directory structure

In the starter kits, React page components live in the `resources/js/pages/` directory.

```
resources/js/
├── app.tsx            # Inertia app entry point
├── bootstrap.ts
├── components/        # Reusable UI components
│   ├── ui/            # shadcn/ui components
│   └── ...
├── hooks/             # Custom React hooks
├── layouts/           # Layout components
│   ├── app-layout.tsx
│   └── auth-layout.tsx
├── lib/               # Utility functions and configuration
├── pages/             # Inertia page components (mirror controller names)
│   ├── auth/
│   │   ├── login.tsx
│   │   └── register.tsx
│   ├── dashboard.tsx
│   └── posts/
│       ├── index.tsx
│       ├── create.tsx
│       └── show.tsx
└── types/             # TypeScript type definitions
```

Writing `Inertia::render('posts/index', [...])` maps to the component at `resources/js/pages/posts/index.tsx`.

***

## JSX syntax basics

React uses JSX. It's a style of writing HTML-like syntax inside JavaScript. Here are the minimum patterns you need to read the starter kit code.

#### `{}` — variable interpolation

Inside JSX, `{}` embeds a JavaScript value or expression.

```tsx theme={null}
const name = 'world'
const count = 3

return (
    <div>
        <p>Hello, {name}!</p>
        <p>Doubled: {count * 2}</p>
    </div>
)
```

#### Conditionals — `&&` and ternaries

React doesn't have a directive equivalent to `v-if`. Use `&&` for simple conditions and the ternary `? :` for if/else.

```tsx theme={null}
{/* Render only when the condition is true */}
{isLoggedIn && <p>Welcome!</p>}

{/* if / else */}
{isLoggedIn ? <p>Welcome!</p> : <a href="/login">Log in</a>}

{/* if / else if / else */}
{isLoggedIn
    ? <p>Welcome!</p>
    : role === 'admin'
        ? <p>Logged in as admin</p>
        : <a href="/login">Log in</a>
}
```

#### Rendering lists — `.map()`

Use `Array.map()` to render lists. Always specify the `key` prop for efficient diffing.

```tsx theme={null}
<ul>
    {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
    ))}
</ul>
```

Equivalent to Vue's `v-for :key` or Svelte's `{#each}`.

#### `className` — CSS class names

Because JSX compiles to JavaScript, `class` is a reserved word. Use `className` for CSS classes.

```tsx theme={null}
{/* HTML: <div class="container"> */}
<div className="container">...</div>
```

#### Event handlers — camelCase

Event attributes in JSX are camelCase and take a function reference.

```tsx theme={null}
<button onClick={handleClick}>Click</button>

{/* Prevent default form submission */}
<form onSubmit={(e) => { e.preventDefault(); handleSubmit() }}>
    ...
</form>
```

***

## Page component basics

Inertia page components are ordinary React components. Data passed from a Laravel controller arrives as props.

### Controller

```php theme={null}
// 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 component

```tsx theme={null}
// 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) returns an object with pagination info too
        // current_page, last_page, per_page, total, etc. are available
    }
}

export default function PostsIndex({ posts }: Props) {
    return (
        <div>
            <h1>Posts</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>
    )
}
```

Just receive the props as component arguments and use the data passed from the controller directly. No REST API definition needed.

***

## The `Link` component

The `<Link>` component provided by `@inertiajs/react` routes page transitions via XHR, avoiding full browser reloads.

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

export default function PostsIndex() {
    return (
        <div>
            {/* Basic link */}
            <Link href="/posts">Posts</Link>

            {/* Link with POST method (e.g. delete) */}
            <Link href="/posts/1" method="delete" as="button">
                Delete
            </Link>

            {/* Preload (fetch ahead of time on hover) */}
            <Link href="/posts/1" preload>
                View post
            </Link>
        </div>
    )
}
```

You write it like a normal `<a>` tag, but Inertia swaps only the page component in the background, giving you an SPA-like feel.

***

## The `Form` component

The `<Form>` component from `@inertiajs/react` is the recommended form submission style used in the starter kit auth screens. Specify `action` and `method` as props, and receive `errors` and `processing` from the children function (render prop).

### Basic usage

```tsx theme={null}
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">Title</label>
                        <input id="title" name="title" type="text" required />
                        {errors.title && <p className="error">{errors.title}</p>}
                    </div>

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

                    <button type="submit" disabled={processing}>
                        {processing ? 'Submitting...' : 'Post'}
                    </button>
                </>
            )}
        </Form>
    )
}
```

`<Form>`'s children are a function `({ errors, processing }) => JSX` (render prop pattern). The `Form` component computes and passes these values automatically. Form fields use HTML-native `name` attributes rather than `onChange` handlers, and the browser's standard form data collection does the work.

### Starter kit pattern

The starter kit uses [Wayfinder](/en/blog/wayfinder-introduction) to manage routes as objects. `store.form()` returns an object containing the route object's `action` and `method`, which is spread onto `<Form>`.

```tsx theme={null}
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 }) => (
                <>
                    {/* Form contents */}
                </>
            )}
        </Form>
    )
}
```

Fields listed in `resetOnSuccess` are automatically reset on successful submission. Use it for fields like password fields that should be cleared after submit.

<Info>
  Without Wayfinder, pass the URL directly like `action="/login"` and it behaves the same.
</Info>

***

## The `useForm` hook

For form handling, use `@inertiajs/react`'s `useForm` hook. It cleanly implements form state management, submission, and validation error display.

### Controller side

```php theme={null}
// 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', 'Post created.');
    }
}
```

### React form component

```tsx theme={null}
// 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>Title</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>Content</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 ? 'Submitting...' : 'Post'}
            </button>
        </form>
    )
}
```

Here are the main properties on the object returned by `useForm`.

| Property / method       | Description                                          |
| ----------------------- | ---------------------------------------------------- |
| `data`                  | The form's data object                               |
| `setData(field, value)` | Update a field's value                               |
| `errors`                | Validation errors (access by field name)             |
| `processing`            | `true` while submitting (used to disable the button) |
| `isDirty`               | `true` if changed from initial values                |
| `post(url)`             | Submit via POST                                      |
| `put(url)`              | Submit via PUT (for updates)                         |
| `delete(url)`           | Submit via DELETE                                    |
| `reset()`               | Reset the form to initial values                     |

When validation errors are returned, `useForm` displays them while preserving the input values.

***

## Shared data

Data needed on every page (the logged-in user, flash messages, etc.) is defined in the `share()` method of the `HandleInertiaRequests` middleware.

```php theme={null}
// 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'),
            ],
        ]);
    }
}
```

Use the `usePage()` hook to access shared data from a React component.

```tsx theme={null}
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>Guest</span>
                )}
            </header>

            {flash.success && (
                <div className="alert-success">{flash.success}</div>
            )}
        </>
    )
}
```

<Info>
  Because shared data is included on every request, keep it to the minimum needed. Wrapping in `fn()` for lazy evaluation ensures it's only evaluated when actually accessed.
</Info>

***

## React hooks essentials

Here are the fundamental React hooks you should know when developing with Inertia × React.

### `useState` — local state

```tsx theme={null}
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` — side effects

```tsx theme={null}
import { useState, useEffect } from 'react'

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

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

        // Cleanup function
        return () => clearInterval(timer)
    }, []) // Empty array = runs once on mount

    return <p>Elapsed: {seconds}s</p>
}
```

### `useMemo` and `useCallback` — performance optimization

```tsx theme={null}
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) {
    // Memoize a value (won't recompute unless posts changes)
    const publishedPosts = useMemo(
        () => posts.filter((post) => post.published),
        [posts],
    )

    // Memoize a function (won't be recreated unless deps change)
    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 support

The React version of the starter kit uses TypeScript by default. Combined with Inertia's type definitions, you get type safety for props.

### Global type definitions

In the starter kit, shared data types are defined in `resources/js/types/index.d.ts`.

```ts theme={null}
// 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
    }
}
```

### Using types in page components

```tsx theme={null}
import { PageProps } from '@/types'

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

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

***

## Summary

React shines when paired with Laravel—especially in a "modern monolith" configuration through Inertia. Its affinity for TypeScript is excellent, making it well-suited to large application development.

| Element              | Role                                             |
| -------------------- | ------------------------------------------------ |
| Laravel controller   | Routing, data retrieval, validation              |
| `Inertia::render()`  | Pass data from controller to React component     |
| React page component | Receive props and render UI                      |
| `useForm`            | Form state management, submission, error display |
| `Link` component     | Page transitions without a full reload           |
| `usePage().props`    | Access shared data                               |
| TypeScript           | Type-safe props and enhanced IDE completion      |

Inertia × React combines the simplicity of a Laravel backend with the powerful React ecosystem, offering a great development experience. Creating a project from the starter kit gets you started immediately, auth screens included.

<Card title="Inertia.js official docs" icon="book-open" href="https://inertiajs.com">
  See the official documentation for all Inertia v3 features.
</Card>


## Related topics

- [Getting started with Svelte — the essentials for using it with Inertia × Laravel](/en/blog/svelte-introduction.md)
- [Getting started with Vue.js — the essentials for using it with Inertia × Laravel](/en/blog/vue-introduction.md)
- [Getting started with Laravel testing using Pest PHP](/en/blog/pest-introduction.md)
- [Installation](/en/installation.md)
- [⚡ Getting started with Livewire 4 — reactive UIs without JavaScript](/en/blog/livewire-introduction.md)
