Best practices for full-stack apps using Laravel 12, Inertia.js, and React.
This skill captures development conventions for projects that combine:
use App\Http\Resources\ProductResource;
public function index()
{
$products = Product::with('store', 'images')->paginate(20);
return inertia('Products/Index', [
'products' => ProductResource::collection($products),
]);
}
class StoreProductRequest extends FormRequest
{
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'price' => 'required|numeric|min:0',
];
}
}
$stores = Store::with(['owner', 'products'])->paginate();
Route::resource('products', ProductController::class);
->through() when transforming nested resourcesuse App\Models\User;
return Inertia::render('Users/Index', [
'users' => User::with('stores')
->get()
->through(fn($user) => [
'id' => $user->id,
'name' => $user->name,
'stores_count' => $user->stores_count,
]),
]);
// AppServiceProvider
Inertia::share([
'auth.user' => fn() => auth()->user() ? auth()->user()->only('id', 'name', 'email') : null,
'flash' => fn() => [
'success' => session('success'),
'error' => session('error'),
],
]);
Inertia::lazy() for heavy datareturn Inertia::render('Products/Index', [
'most_expensive' => Inertia::lazy(fn() => ProductResource::collection(Product::expensive()->take(5)->get())),
]);
redirect()->route() and Inertia::location() properlyreturn redirect()->route('products.index')->with('success', 'Created');
$product = Product::create($request->validated());
import { InertiaLink, usePage } from '@inertiajs/inertia-react';
type Product = { id: number; name: string; price: number };
type Props = { products: Product[]; auth: { user: { name: string } | null } };
export default function ProductsIndex() {
const { props } = usePage<{ props: Props }>();
const { products, auth } = props;
return (
<div>
<h1>Hi {auth.user?.name}</h1>
{products.map((product) => <div key={product.id}>{product.name}</div>)}
</div>
);
}
useForm from Inertia for optimistic UI and errorsimport { useForm } from '@inertiajs/inertia-react';
function ProductCreate() {
const { data, setData, post, processing, errors } = useForm({
name: '',
price: 0,
});
const submit = (e: React.FormEvent) => {
e.preventDefault();
post('/products');
};
return (
<form onSubmit={submit}>
<input value={data.name} onChange={(e) => setData('name', e.target.value)} />
{errors.name && <span>{errors.name}</span>}
<button disabled={processing}>Save</button>
</form>
);
}
React.memo and useMemo for expensive transformspublic function update(User $user, Product $product)
{
return $user->id === $product->user_id;
}
Usage in controller:
$this->authorize('update', $product);
Route::middleware(['auth', 'verified'])->group(function () {
Route::resource('products', ProductController::class);
});
use function Inertia\assert;
$this->actingAs($user)
->get(route('products.index'))
->assertInertia(fn (Assert $page) =>
$page->component('Products/Index')
->has('products')
);
usePage() and InertiaLinkInertia.post when form submitsonce in Inertia for transient props:Inertia::share('flash', fn() => [
'success' => session()->pull('success'),
]);
php artisan route:cache
php artisan config:cache
useForm