Si tu frontend está hecho con React, en este tutorial verás cómo añadirle a una aplicación de Laravel un acceso sin contraseña. El funcionamiento es sencillo: el usuario indica su email, le llega un código de seis cifras y, al escribirlo, entra en su cuenta, que se crea de forma automática la primera vez.
Para conectar React con Laravel usaremos Inertia, que nos permite escribir las pantallas como componentes de React mientras las rutas, la validación y la sesión siguen en manos de Laravel.
Introducción
Esta es la versión para React de una serie de tutoriales sobre el mismo login. Toda la lógica del código de acceso, desde cómo se genera y se guarda hasta los límites de envío, está explicada en el tutorial de cómo crear un login sin contraseña en Laravel, y la configuración de Inertia en el servidor coincide con la del tutorial de cómo crear un login sin contraseña con Laravel, Inertia y Vue. Aquí nos centraremos en la parte de React.
Contenidos
La protección frente a bots corre a cargo de Cloudflare Turnstile. Para mostrarlo en React usaremos el componente que incluye Laracaptcha, que se encarga de cargar el script de Cloudflare, dibujar el widget y entregarnos el token.
Si es la primera vez que trabajas con React, te recomiendo empezar por aquí:
- Aprende React: Tutorial de introducción a React
- Componentes con estado: Cómo crear una aplicación con React usando Hooks
Todo el código está en el repositorio del proyecto en GitHub. Así se ve la pantalla de acceso una vez terminada:

Preparación del proyecto
Comprueba antes que tienes PHP 8.3 o una versión posterior, composer y Node.js en tu equipo.
Instala Laravel, Inertia y React
Partimos de un proyecto de Laravel nuevo, sin kit de inicio, ya que los kits traen un login con contraseña que no nos sirve:
composer create-project laravel/laravel login-react
cd login-react
composer require inertiajs/inertia-laravel edulazaro/laracaptcha
npm install react react-dom @inertiajs/react @vitejs/plugin-react
Con el primer composer require añadimos el adaptador de Inertia para Laravel y Laracaptcha, y con npm install instalamos React, su adaptador de Inertia y el plugin que permite a Vite compilar JSX.
Configura el servidor
En la parte de Laravel, Inertia necesita su middleware y una vista raíz. Genera el middleware con php artisan inertia:middleware, regístralo en bootstrap/app.php y comparte en él el usuario y los mensajes de estado. La vista raíz resources/views/app.blade.php es igual que en la versión con Vue, salvo por una línea:
@viteReactRefresh
@vite('resources/js/app.jsx')
@inertiaHead
La directiva @viteReactRefresh activa la recarga en caliente de React mientras trabajas con npm run dev, de modo que los cambios en los componentes se ven al instante sin perder su estado. Debe ir antes de @vite.
Configura Vite
En el archivo vite.config.js, el punto de entrada pasa a ser un archivo .jsx y el plugin es el de React. El alias @laracaptcha apunta a los componentes que Laracaptcha incluye dentro del paquete de composer:
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
laravel({ input: ['resources/js/app.jsx'], refresh: true }),
react(),
],
resolve: {
alias: {
'@laracaptcha': new URL('./vendor/edulazaro/laracaptcha/resources/js', import.meta.url).pathname,
},
},
});
Arranca React con Inertia
Elimina el archivo resources/js/app.js y crea en su lugar resources/js/app.jsx:
import { createInertiaApp } from '@inertiajs/react';
import { createRoot } from 'react-dom/client';
const appName = import.meta.env.VITE_APP_NAME;
createInertiaApp({
title: (title) => (title ? `${title} · ${appName}` : appName),
resolve: (name) => {
const pages = import.meta.glob('./pages/**/*.jsx', { eager: true });
return pages[`./pages/${name}.jsx`];
},
setup({ el, App, props }) {
createRoot(el).render(<App {...props} />);
},
});
En la función setup, createRoot crea la raíz de React sobre el elemento que genera la directiva @inertia, y a partir de ahí Inertia se ocupa de cambiar de página. Cada vez que el servidor renderiza una página, por ejemplo auth/Login, la función resolve carga el componente resources/js/pages/auth/Login.jsx.
El controlador
El controlador LoginController es exactamente el mismo que en la versión con Vue. Recuerda los dos puntos que lo diferencian de la versión con Blade: devuelve páginas con Inertia::render y le pasa a la página del login el resultado de Captcha::props():
return Inertia::render('auth/Login', ['captcha' => Captcha::props()]);
El token llega en un campo llamado captcha, que se valida con la regla CaptchaRule de Laracaptcha. Gracias a que el proveedor viaja en esas props, el mismo componente de React funcionará tanto con Turnstile como con reCAPTCHA v2 o v3, según lo que indique el .env.
Los componentes de React
Las páginas de la aplicación son componentes de React que reciben como props los datos que les envía el controlador.
Crea la página del email
La página resources/js/pages/auth/Login.jsx recibe la prop captcha y muestra el formulario:
import { Head, useForm } from '@inertiajs/react';
import { useRef } from 'react';
import { Captcha } from '@laracaptcha/react';
export default function Login({ captcha }) {
const { data, setData, post, processing, errors } = useForm({ email: '', captcha: '' });
const captchaWidget = useRef(null);
function submit(event) {
event.preventDefault();
post('/login', {
onError: () => captchaWidget.current?.reset(),
});
}
return (
<>
<Head title="Entrar" />
<main className="card">
<h1>Entrar</h1>
<form onSubmit={submit}>
<label htmlFor="email">Email</label>
<input id="email" type="email" value={data.email} onChange={(event) => setData('email', event.target.value)} required autoFocus />
{errors.email && <p className="error">{errors.email}</p>}
<Captcha ref={captchaWidget} {...captcha} language="es" onToken={(token) => setData('captcha', token)} />
{errors.captcha && <p className="error">{errors.captcha}</p>}
<button type="submit" disabled={processing}>Enviarme el código</button>
</form>
</main>
</>
);
}
Veamos qué hace cada parte:
useForm: El hook de formularios de Inertia nos devuelve los datos del formulario, la funciónsetDatapara modificarlos,postpara enviarlos y los errores de validación del servidor. Más detalles en la guía de formularios de Inertia.{...captcha}: Al expandir la prop, el componente recibe el proveedor, la clave pública y la URL del script.onToken: React no tiene un equivalente av-model, así que el componente nos avisa con esta función cada vez que cambia el token. Al resolver el desafío recibimos el token, y si caduca o falla recibimos una cadena vacía, por lo que el formulario nunca envía un token inválido.ref: Nos da acceso al métodoresetdel componente. Como cada token solo se puede verificar una vez, cuando el servidor rechaza el envío pedimos otro desdeonError.
Con reCAPTCHA v3 no se muestra ningún desafío: el componente obtiene el token al montarse y lo renueva antes de que caduque, y onToken funciona igual.
Crea la página del código
En resources/js/pages/auth/Code.jsx usamos dos instancias de useForm, una para comprobar el código y otra para pedir uno nuevo. Esta vez no desestructuramos el resultado, ya que necesitamos distinguir los datos de cada formulario:
export default function Code({ email }) {
const { status } = usePage().props;
const form = useForm({ code: '' });
const resend = useForm({});
function submit(event) {
event.preventDefault();
form.post('/login/codigo');
}
function sendAnother() {
resend.post('/login/codigo/reenviar', {
onSuccess: () => form.reset('code'),
});
}
const error = form.errors.code || resend.errors.code;
// ...
}
El hook usePage da acceso a las props compartidas por el middleware, como el mensaje que el controlador guarda en la sesión al reenviar el código. En el campo del código usamos inputMode="numeric" y autoComplete="one-time-code", para que el móvil muestre el teclado numérico y sugiera el código que acaba de llegar:

Crea el panel
El panel lee el usuario de las props compartidas y cierra la sesión enviando una petición con el router de Inertia:
import { Head, router, usePage } from '@inertiajs/react';
export default function Dashboard() {
const { user } = usePage().props.auth;
return (
<main className="card">
<h1>Hola, {user.name}</h1>
<button type="button" onClick={() => router.post('/logout')}>Cerrar sesión</button>
</main>
);
}
Compila los componentes con npm run build y arranca el servidor con php artisan serve para probar la aplicación.
Tests
Los tests se escriben en PHP, con las aserciones de Inertia para comprobar qué componente de React renderiza cada ruta y con qué props. Así nos aseguramos, por ejemplo, de que el panel recibe el usuario correcto:
$this->actingAs($user)->get('/panel')->assertInertia(fn (Assert $page) => $page
->component('Dashboard')
->where('auth.user.email', $user->email));
En los tests, Captcha::fake() hace que las props del captcha no lleven script, de modo que el componente no se dibuja y no se contacta con Cloudflare. El repositorio incluye once tests y puedes ampliar información en la documentación de testing de Inertia.
Finalizando
Con esto tendrás tu login sin contraseña funcionando con Laravel, Inertia y React. Para publicarlo necesitarás un servidor de correo real, tus propias claves de Turnstile y los registros SPF de tu dominio, además de ejecutar npm run build al desplegar.
Si quieres comparar enfoques, estas son las demás versiones de este login:
- Blade: Cómo crear un login sin contraseña en Laravel
- Livewire: Cómo crear un login sin contraseña con Laravel y Livewire
- Vue: Cómo crear un login sin contraseña con Laravel, Inertia y Vue
- Svelte: Cómo crear un login sin contraseña con Laravel, Inertia y Svelte
Puedes consultar el código del proyecto en GitHub, y las demás opciones del paquete en la documentación de Laracaptcha.
Esto ha sido todo. ¡Espero que te resulte útil!