Flash Messages y Manejo de Errores (Inertia)¶
Esta guía explica el manejo consistente de mensajes flash y errores en el backend Laravel para una UX uniforme con Inertia.js y Sonner.
Arquitectura¶
Shared Data con Flash Messages¶
El middleware HandleInertiaRequests
proporciona datos compartidos automáticamente a todas las páginas Inertia:
// app/Http/Middleware/HandleInertiaRequests.php
public function share(Request $request): array
{
return [
// ... otros datos
// Flash messages para toasts con Sonner
'flash' => [
'success' => fn () => $request->session()->get('success'),
'error' => fn () => $request->session()->get('error'),
'info' => fn () => $request->session()->get('info'),
],
// Request ID para tracking y debugging
'requestId' => $request->attributes->get('request_id'),
];
}
Referencias:
Patrón de Redirects con Flash¶
Los controladores que usan HandlesIndexAndExport
siguen un patrón consistente:
// ✅ Éxito: redirect con flash success
return $this->ok('users.index', [], '3 usuarios eliminados exitosamente');
// ❌ Error: redirect con flash error
return $this->fail('users.index', [], 'Error durante la operación');
Esto genera redirects HTTP 303 (See Other) que Inertia convierte automáticamente en visitas de página.
Implementación en Controladores¶
Helper Methods¶
El trait HandlesIndexAndExport
proporciona helpers para redirects consistentes:
/**
* Redirigir con mensaje de éxito
*/
protected function ok(string $routeName, array $params = [], ?string $message = null): RedirectResponse
{
$redirect = redirect()->route($routeName, $params);
if ($message !== null) {
$redirect->with('success', $message);
}
return $redirect;
}
/**
* Redirigir con mensaje de error
*/
protected function fail(string $routeName, array $params = [], ?string $message = null): RedirectResponse
{
$redirect = redirect()->route($routeName, $params);
if ($message !== null) {
$redirect->with('error', $message);
}
return $redirect;
}
Operaciones Bulk¶
Las operaciones masivas usan redirects en lugar de JSON responses:
public function bulk(Request $request): RedirectResponse
{
// ... validación y autorización
try {
$count = $this->service->bulkDeleteByIds($ids);
$message = sprintf('%d registro(s) eliminados exitosamente', $count);
return $this->ok($this->indexRouteName(), [], $message);
} catch (DomainActionException $e) {
return $this->fail($this->indexRouteName(), [], $e->getMessage());
} catch (\Exception $e) {
return $this->fail($this->indexRouteName(), [], 'Error durante la operación masiva');
}
}
Export con Error Handling¶
Las exportaciones manejan errores redirigiendo al index:
public function export(BaseIndexRequest $request): StreamedResponse|RedirectResponse
{
$this->authorize('export', $this->policyModel());
try {
// ... lógica de exportación
return $this->service->export($dto, $format);
} catch (DomainActionException $e) {
return $this->fail($this->indexRouteName(), [], $e->getMessage());
} catch (\Exception $e) {
return $this->fail($this->indexRouteName(), [], 'Error durante la exportación');
}
}
Exception Handler¶
El manejador de excepciones convierte DomainActionException
en redirects con flash para requests de Inertia:
// bootstrap/app.php
->withExceptions(function (Exceptions $exceptions) {
$exceptions->renderable(function (\App\Exceptions\DomainActionException $e, \Illuminate\Http\Request $request) {
// Si es una request de Inertia, redirigir con flash error
if ($request->hasHeader('X-Inertia')) {
return back()->with('error', $e->getMessage());
}
// Para requests normales, usar el comportamiento por defecto
return null;
});
})
Frontend Integration¶
Layout con Sonner¶
El layout de React debe mostrar los toasts automáticamente:
// resources/js/layouts/AppLayout.tsx
import { Toaster, toast } from 'sonner';
import { usePage } from '@inertiajs/react';
export default function AppLayout({ children }) {
const { flash } = usePage().props;
// Mostrar toasts cuando hay flash messages
React.useEffect(() => {
if (flash.success) {
toast.success(flash.success);
}
if (flash.error) {
toast.error(flash.error);
}
if (flash.info) {
toast.info(flash.info);
}
}, [flash]);
return (
<div>
{children}
<Toaster position="top-right" />
</div>
);
}
Operaciones Bulk desde Frontend¶
Las operaciones bulk se envían como formularios normales:
// Ejemplo: Eliminar usuarios seleccionados
const handleBulkDelete = (selectedIds: number[]) => {
router.post(route('users.bulk'), {
action: 'delete',
ids: selectedIds,
});
// El redirect + flash se maneja automáticamente
};
Validación vs Flash Messages¶
⚠️ Importante: La validación Laravel sigue usando errores de campo, NO flash messages:
- Validación (422): Muestra errores específicos por campo
- Flash Messages: Para resultados de operaciones (éxito/error general)
// ❌ NO hacer esto para errores de validación
return $this->fail('users.index', [], 'El email es requerido');
// ✅ Dejar que Laravel maneje validación normalmente
$request->validate(['email' => 'required']);
Referencias Inertia¶
- Shared Data - Datos disponibles en todas las páginas
- Redirects - Cómo Inertia maneja redirects HTTP 303
- Validation - Manejo de errores de validación
Casos de Uso¶
✅ Usar Flash Messages Para:¶
- Confirmaciones de operaciones bulk
- Resultados de exportación fallida
- Errores de negocio/dominio
- Mensajes informativos generales
❌ NO Usar Flash Messages Para:¶
- Errores de validación de formularios
- Errores HTTP (404, 500, etc.)
- Estados de carga/loading
Testing¶
Los tests pueden verificar redirects y flash messages:
$response = $this->post('/users/bulk', [
'action' => 'delete',
'ids' => [1, 2]
]);
$response->assertRedirect(route('users.index'));
$response->assertSessionHas('success', '2 registro(s) eliminados exitosamente');
Ejemplo Completo¶
class UserController extends Controller
{
use HandlesIndexAndExport;
public function bulk(Request $request): RedirectResponse
{
$this->authorize('update', User::class);
$request->validate([
'action' => 'required|in:delete,restore',
'ids' => 'required|array|min:1',
'ids.*' => 'integer|exists:users,id',
]);
try {
$count = match($request->action) {
'delete' => $this->userService->bulkDelete($request->ids),
'restore' => $this->userService->bulkRestore($request->ids),
};
$message = sprintf('%d usuario(s) %s exitosamente',
$count,
$request->action === 'delete' ? 'eliminados' : 'restaurados'
);
return $this->ok('users.index', [], $message);
} catch (DomainActionException $e) {
return $this->fail('users.index', [], $e->getMessage());
}
}
protected function indexRouteName(): string
{
return 'users.index';
}
}
Esta arquitectura garantiza una UX consistente y predecible en toda la aplicación.