Saltar al contenido principal

¿Qué es React?

React es una biblioteca de JavaScript para construir interfaces de usuario, desarrollada y mantenida por Meta (antes Facebook). Se caracteriza por su descripción declarativa de la UI y una arquitectura basada en componentes, y se utiliza en un amplio rango: desde pequeños widgets hasta SPAs completas. El núcleo de React es el renderizado eficiente a través del DOM virtual. Cuando cambia el estado (state), React refleja en el DOM solo la diferencia, así que no hace falta manipular manualmente el DOM.
En esta página se explica la combinación de React 19 e Inertia v3. Los starter kits de Laravel 13 usan esta configuración por defecto.

JSX y TSX

Los componentes de React se escriben con la sintaxis JSX (JavaScript XML). Permite escribir una notación parecida a HTML directamente dentro de JavaScript.
// Ejemplo de JSX
function Greeting({ name }) {
    return <h1>Hola, {name}</h1>
}
En los starter kits se adopta TypeScript (.tsx) como estándar. Con las definiciones de tipo se mejora el autocompletado del IDE y se detectan bugs de forma temprana.
// Ejemplo de TSX (TypeScript)
type Props = {
    name: string
}

function Greeting({ name }: Props) {
    return <h1>Hola, {name}</h1>
}
Los starter kits de React de Laravel usan TypeScript + TSX como estándar. Todos los ejemplos de esta página también están en TSX.

Posición dentro de Laravel

Historia

La relación entre React y Laravel es algo más reciente que la de Vue, pero actualmente tiene un tratamiento igual o superior. En Laravel 6 (2019) el scaffold de autenticación se separó al paquete laravel/ui, y junto con Vue se ofreció también un scaffold para React. Sin embargo, en aquel momento Vue era predominante y la presencia de la versión React era escasa. Con la incorporación del stack Inertia + React en Laravel Breeze (2021) comenzó su adopción real, y con la renovación de los starter kits de Laravel 12 (2025), React pasó a estar en pie de igualdad con Vue (más aún, apareciendo el primero).

Estilo dominante actual: Inertia × React

El uso central de React en Laravel actualmente es Inertia × React. Inertia permite realizar una arquitectura de “monolito moderno” en la que se pasan datos directamente desde los controladores de Laravel a componentes de React sin diseñar una API.

Configuración

Vía starter kit (recomendado)

Al iniciar un proyecto nuevo, usar el starter kit es lo más sencillo.
laravel new my-app
Si eliges React en el prompt interactivo, se configura automáticamente todo lo siguiente.
  • inertiajs/inertia-laravel (adaptador del lado servidor)
  • @inertiajs/react (adaptador del lado cliente)
  • react + react-dom (React 19)
  • @vitejs/plugin-react (plugin de Vite)
  • TypeScript + @types/react
  • Tailwind CSS + biblioteca de componentes shadcn/ui
  • Middleware HandleInertiaRequests
  • Pantallas de autenticación como login y registro (ya implementadas con Inertia + React + TypeScript)

Instalación manual

Si vas a añadirlo a un proyecto existente, instala por separado el lado servidor y el lado cliente.
# Lado servidor (PHP)
composer require inertiajs/inertia-laravel

# Lado cliente (JavaScript)
npm install @inertiajs/react react react-dom
npm install --save-dev @vitejs/plugin-react @types/react @types/react-dom typescript
A continuación, añade el plugin de React en 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(),
    ],
})
Arranca la app Inertia en 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} />)
    },
})
Para los detalles de la instalación manual (configuración de la plantilla raíz, registro del middleware, etc.), consulta la documentación oficial de Inertia.

Estructura de directorios

En los starter kits, los componentes de página de React se colocan en el directorio resources/js/pages/.
resources/js/
├── app.tsx            # Punto de entrada de la app Inertia
├── bootstrap.ts
├── components/        # Componentes de UI reutilizables
│   ├── ui/            # Componentes shadcn/ui
│   └── ...
├── hooks/             # Hooks de React personalizados
├── layouts/           # Componentes de layout
│   ├── app-layout.tsx
│   └── auth-layout.tsx
├── lib/               # Utilidades y configuración
├── pages/             # Componentes de página de Inertia (corresponden al nombre del controlador)
│   ├── auth/
│   │   ├── login.tsx
│   │   └── register.tsx
│   ├── dashboard.tsx
│   └── posts/
│       ├── index.tsx
│       ├── create.tsx
│       └── show.tsx
└── types/             # Definiciones de tipo TypeScript
Cuando escribes Inertia::render('posts/index', [...]), el componente correspondiente es resources/js/pages/posts/index.tsx.

Fundamentos de la sintaxis JSX

React usa JSX. Es un estilo en el que se escribe sintaxis parecida a HTML dentro de JavaScript. Presentamos los patrones mínimos que conviene conocer para leer el código de los starter kits.

{} — Interpolación de variables

Dentro de JSX se usa {} para incrustar valores o expresiones de JavaScript.
const name = 'mundo'
const count = 3

return (
    <div>
        <p>Hola, {name}!</p>
        <p>El doble es {count * 2}</p>
    </div>
)

Condicionales — && y operador ternario

React no tiene una directiva equivalente a v-if. Para condicionales simples usa el operador &&, y para if/else, el operador ternario ? :.
{/* Se muestra solo cuando la condición es true */}
{isLoggedIn && <p>¡Bienvenido!</p>}

{/* if / else */}
{isLoggedIn ? <p>¡Bienvenido!</p> : <a href="/login">Iniciar sesión</a>}

{/* if / else if / else */}
{isLoggedIn
    ? <p>¡Bienvenido!</p>
    : role === 'admin'
        ? <p>Sesión iniciada como administrador</p>
        : <a href="/login">Iniciar sesión</a>
}

Renderizado de listas — .map()

Para renderizar listas se usa Array.map(). Para permitir un diff eficiente, hay que indicar siempre la prop key.
<ul>
    {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
    ))}
</ul>
Es el equivalente a v-for :key de Vue o {#each} de Svelte.

className — Nombre de clase CSS

Como JSX se compila a JavaScript, class es palabra reservada. Para las clases CSS se usa className.
{/* HTML: <div class="container"> */}
<div className="container">...</div>

Manejadores de eventos — camelCase

Los atributos de evento en JSX se escriben en camelCase, y se les pasa la referencia a una función.
<button onClick={handleClick}>Clic</button>

{/* Cancelar el comportamiento por defecto del envío del formulario */}
<form onSubmit={(e) => { e.preventDefault(); handleSubmit() }}>
    ...
</form>

Fundamentos del componente de página

Los componentes de página de Inertia son componentes React normales. Los datos pasados desde el controlador de Laravel se reciben como props.

Controlador

// 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),
        ]);
    }
}

Componente de página React

// 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) devuelve un objeto que incluye también información de paginación
        // También se pueden usar current_page, last_page, per_page, total, etc.
    }
}

export default function PostsIndex({ posts }: Props) {
    return (
        <div>
            <h1>Lista de publicaciones</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>
    )
}
Solo con recibir las props como argumento del componente puedes usar tal cual los datos pasados desde el controlador. No hace falta definir una API REST.
Al usar el componente <Link> que ofrece @inertiajs/react, la navegación entre páginas se hace por XHR y se evita la recarga completa del navegador.
import { Link } from '@inertiajs/react'

export default function PostsIndex() {
    return (
        <div>
            {/* Enlace básico */}
            <Link href="/posts">Lista de publicaciones</Link>

            {/* Enlace con método POST (para borrar, etc.) */}
            <Link href="/posts/1" method="delete" as="button">
                Borrar
            </Link>

            {/* Preload (precarga al pasar el ratón) */}
            <Link href="/posts/1" preload>
                Ver publicación
            </Link>
        </div>
    )
}
Se escribe igual que una etiqueta <a> normal, pero por debajo Inertia solo cambia el componente de página, con lo que se logra una sensación de operación como en una SPA.

Componente Form

El componente <Form> que ofrece @inertiajs/react es el estilo recomendado de envío de formularios que se usa en las pantallas de autenticación del starter kit. Se indican action y method como props, y en la función children (render prop) se reciben errors y processing.

Uso básico

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">Título</label>
                        <input id="title" name="title" type="text" required />
                        {errors.title && <p className="error">{errors.title}</p>}
                    </div>

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

                    <button type="submit" disabled={processing}>
                        {processing ? 'Enviando...' : 'Publicar'}
                    </button>
                </>
            )}
        </Form>
    )
}
El children de <Form> es una función del tipo ({ errors, processing }) => JSX (patrón render prop). El componente Form calcula automáticamente esos valores y te los pasa. En los campos del formulario, en lugar de un handler onChange, se usa el atributo nativo name de HTML, y funciona la recolección estándar de datos de formulario del navegador.

Patrón del starter kit

El starter kit gestiona las rutas como objetos usando Wayfinder. store.form() devuelve un objeto que incluye action y method del objeto de ruta, y se aplica al <Form> con spread.
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 }) => (
                <>
                    {/* Contenido del formulario */}
                </>
            )}
        </Form>
    )
}
Los campos indicados en resetOnSuccess se resetean automáticamente al enviar con éxito. Se indican en campos que se quieren dejar vacíos tras el envío, como la contraseña.
Si no usas Wayfinder, pasando directamente la URL con action="/login" funciona igual.

Hook useForm

Para el procesamiento de formularios se usa el hook useForm de @inertiajs/react. La gestión del estado del formulario, el envío y la visualización de errores de validación se implementan de forma sencilla.

Lado del controlador

// 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', 'Se ha creado la publicación.');
    }
}

Componente de formulario React

// 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>Título</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>Contenido</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 ? 'Enviando...' : 'Publicar'}
            </button>
        </form>
    )
}
Resumen de las principales propiedades del objeto que devuelve useForm.
Propiedad / MétodoDescripción
dataObjeto de datos del formulario
setData(field, value)Actualiza el valor de un campo
errorsErrores de validación (acceso por nombre de campo)
processingtrue durante el envío (útil para desactivar el botón)
isDirtytrue si se ha cambiado respecto al valor inicial
post(url)Enviar con petición POST
put(url)Enviar con petición PUT (actualización)
delete(url)Enviar con petición DELETE
reset()Resetear el formulario a los valores iniciales
Cuando se devuelve un error de validación, useForm mantiene el contenido introducido y muestra el error.

Datos compartidos (Shared Data)

Los datos comunes a todas las páginas (información del usuario autenticado, mensajes flash, etc.) se definen en el método share() del 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'),
            ],
        ]);
    }
}
Para acceder a los datos compartidos desde un componente React se usa el hook usePage().
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>Invitado</span>
                )}
            </header>

            {flash.success && (
                <div className="alert-success">{flash.success}</div>
            )}
        </>
    )
}
Como los datos compartidos se incluyen en todas las peticiones, se recomienda limitarlos al mínimo necesario. Si los pasas mediante una evaluación diferida con fn(), solo se evalúan cuando realmente se acceden.

Fundamentos de los hooks de React

Presentamos los hooks básicos de React que conviene conocer para desarrollar con Inertia × React.

useState — Gestión de estado local

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 — Procesamiento de efectos secundarios

import { useState, useEffect } from 'react'

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

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

        // Función de limpieza
        return () => clearInterval(timer)
    }, []) // Array vacío = se ejecuta una sola vez en el mount

    return <p>Tiempo transcurrido: {seconds}s</p>
}

useMemo y useCallback — Optimización de rendimiento

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) {
    // Memoiza el valor (no se recalcula mientras posts no cambie)
    const publishedPosts = useMemo(
        () => posts.filter((post) => post.published),
        [posts],
    )

    // Memoiza la función (no se recrea mientras las dependencias no cambien)
    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>
    )
}

Soporte de TypeScript

La versión React de los starter kits usa TypeScript por defecto. Combinado con las definiciones de tipo de Inertia, se garantiza la seguridad de tipos de las props.

Definiciones de tipo globales

En los starter kits, los tipos de los datos compartidos se definen en resources/js/types/index.d.ts.
// 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
    }
}

Uso de tipos en el componente de página

import { PageProps } from '@/types'

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

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

Conclusión

React saca su potencial al combinarse con Laravel, especialmente en la configuración de “monolito moderno” a través de Inertia. Su compatibilidad con TypeScript también es excelente, y encaja bien con el desarrollo de aplicaciones a gran escala.
ElementoRol
Controlador de LaravelEnrutamiento, obtención de datos y validación
Inertia::render()Pasa datos del controlador al componente React
Componente de página ReactRecibe props y renderiza la UI
useFormGestión del estado del formulario, envío y visualización de errores
Componente LinkNavegación entre páginas sin recarga completa
usePage().propsAcceso a los datos compartidos
TypeScriptSeguridad de tipos de props y mejor autocompletado del IDE
Con Inertia × React obtienes una experiencia de desarrollo que combina la sencillez del backend Laravel con el potente ecosistema de React. Si creas el proyecto con el starter kit, puedes empezar a desarrollar de inmediato con las pantallas de autenticación incluidas.

Documentación oficial de Inertia.js

Para todas las funcionalidades de Inertia v3, consulta la documentación oficial.
Última modificación el 13 de julio de 2026