By Team
ScriptlyStore Core Engine
React 19 and Next.js 15 have finalized server-centric APIs that fundamentally change how we build web applications. By merging the boundaries between the server and the client, these updates allow developers to build incredibly fast, responsive, and secure applications with less boilerplate.
This guide explores the key paradigms introduced in this release and provides step-by-step instructions on implementing Server Actions, async transitions, and optimistic state updates in your SaaS applications.
| Feature | Pre-React 19 | React 19 / Next.js 15 | Benefit |
|---|---|---|---|
| Data Mutations | API Route handlers + fetch | Server Actions ('use server') | No endpoint configuration, type safety |
| Pending States | Manual isLoading states | useTransition / Actions | Declarative loading states |
| Optimistic Updates | Complex state logic | useOptimistic hook | Instant user feedback on slow networks |
| Component Fetching | Client-side useEffect | Async Server Components | Zero client-side JavaScript, direct database queries |
Server Actions allow you to define server-side logic that can be invoked directly from client-side components. They are secure, type-safe, and automatically handle POST requests behind the scenes.
Create an action file at src/lib/actions/products.ts:
'use server';
import { db } from "@/db";
import { products } from "@/db/schema";
import { revalidatePath } from "next/cache";
export async function updateProductPrice(productId: string, newPricePaise: number) {
try {
// Perform authorization checks on the server
const user = await checkUserAuth();
if (!user || user.role !== 'admin') {
throw new Error("Unauthorized");
}
await db
.update(products)
.set({ price: newPricePaise })
.where(eq(products.id, productId));
revalidatePath("/admin/products");
return { success: true };
} catch (error: any) {
return { success: false, error: error.message };
}
}React 19 introduces useActionState (formerly useFormState) to manage form actions with built-in pending state indicators.
'use client';
import { useActionState } from "react";
import { updateProductPrice } from "@/lib/actions/products";
export function PriceEditor({ productId, currentPrice }: { productId: string; currentPrice: number }) {
const [state, formAction, isPending] = useActionState(
async (prevState: any, formData: FormData) => {
const price = Number(formData.get("price")) * 100;
const res = await updateProductPrice(productId, price);
return res;
},
null
);
return (
<form action={formAction} className="space-y-4">
<input type="number" name="price" defaultValue={currentPrice / 100} className="border p-2 rounded" />
<button type="submit" disabled={isPending} className="bg-primary text-white p-2 rounded">
{isPending ? "Saving..." : "Update Price"}
</button>
{state && !state.success && <p className="text-red-500">{state.error}</p>}
</form>
);
}For the ultimate premium experience, use the useOptimistic hook to update the UI instantly before the server acknowledges the transaction.
'use client';
import { useOptimistic, startTransition } from "react";
import { updateProductPrice } from "@/lib/actions/products";
export function OptimisticPrice({ productId, initialPrice }: { productId: string; initialPrice: number }) {
const [optimisticPrice, setOptimisticPrice] = useOptimistic(
initialPrice,
(state, newPrice: number) => newPrice
);
const handleUpdate = async (formData: FormData) => {
const newPrice = Number(formData.get("price")) * 100;
// Instantly update UI optimistically
startTransition(() => {
setOptimisticPrice(newPrice);
});
// Run actual server update
await updateProductPrice(productId, newPrice);
};
return (
<form action={handleUpdate} className="flex gap-2">
<span className="font-bold">Price: `$${(optimisticPrice / 100).toFixed(2)}`</span>
<input type="number" name="price" placeholder="New price" className="border px-2 py-1 text-xs" />
<button type="submit" className="bg-blue-500 text-white text-xs px-2 py-1">Save</button>
</form>
);
}