Requerimientos
- Bases de Nextjs
Que son los Server Actions
- Permite crear mutaciones desde el lado del servidor sin la necesidad de crear Endpoints de API Inncesarios
- Reduce la cantidad de codigo de Javascript del lado cliente
- Soporta la mejora de formularios de forma progresiva
- Server Actions puede funcionar dentro de componentes del lado Servidor (Server Components) y tambien desde componnetes del lado Cliente.
Creación de un proyecto de Nextjs
npx create-next-app nextjs-server-actions
o mas resumido:
npx create-next-app --ts --tailwind --app --src-dir --eslint next-server-actions
Luego de haber creado el proyecto, actualiza el src/index.css de la siguiente forma:
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
@apply bg-gray-950
}
Creación de Formulario Básico
export default function App() {
return (
<div className="flex flex-col items-center justify-center h-screen">
<form>
<input
type="text"
placeholder="name"
className="border border-gray-300 rounded-md p-2 block mb-2"
/>
<input
type="text"
placeholder="price"
className="border border-gray-300 rounded-md p-2 block"
/>
<button className="bg-blue-500 text-white rounded-md p-2 mt-2 w-full">
Create Product
</button>
</form>
</div>
);
}
Usando este formulario creeriamos que necesitamos una api o usar un "use client" para poder manejar el estado y eventos del formulario, pero podemos usar solo codigo de servidor para manejar esto.
Server Action
Actualizaremos el formulario de esta forma:
import { randomUUID } from "crypto";
export default function App() {
const saveProduct = async (formData: FormData) => {
'use server'
console.log(formData);
const name = formData.get("name")?.toString();
const price = formData.get("price")?.toString();
if (!name || !price) {
return;
}
const newProduct = {
name,
price: parseInt(price),
id: randomUUID(),
};
console.log(newProduct);
// save to database or send to an API
};
return (
<div className="flex flex-col items-center justify-center h-screen">
<form action={saveProduct}>
<input
type="text"
name="name"
placeholder="name"
className="border border-gray-300 rounded-md p-2 block mb-2"
/>
<input
type="text"
name="price"
placeholder="price"
className="border border-gray-300 rounded-md p-2 block"
/>
<button className="bg-blue-500 text-white rounded-md p-2 mt-2 w-full">
Create Product
</button>
</form>
</div>
);
}
en donde estamos usando lo siguiente
- todos los inputs tienen una propiedad name
- se ejecuta el action
- la funcion que maneja el action recibe toda la informacion del formulario de tipo FormData
- dentro de la funcion saveProduct estamos usando un 'use server' para indicar que se procesara en el servidor
- los server actions deben ser funciones async
Cuando se envia un formulario con un server action,
Server Actions pueden ser componibles
lo que quiere decir que se pueden mover a un archivo por aparte
crea un archivo en src/actions/serverActions.ts
"use server";
import { randomUUID } from "crypto";
export const saveProduct = async (formData: FormData) => {
console.log(formData);
const name = formData.get("name")?.toString();
const price = formData.get("price")?.toString();
if (!name || !price) {
return;
}
const newProduct = {
name,
price: parseInt(price),
id: randomUUID(),
};
console.log(newProduct);
// save to database or send to an API
};
- Aqui se ha movido el use server fuera de la funcion, lo que indica que cualquier funcion debajo de esta declaracion es un server action
Server Actions del lado cliente
Primero vamos a crear una copia del formulario para poder empezar a compararlo con la opcion del lado del servidor:
"use client";
import { saveProduct } from "@/actions/serveActions";
export default function App() {
return (
<div className="flex flex-col items-center justify-center h-screen">
<form action={(e) => saveProduct(e)}>
<input
type="text"
name="name"
placeholder="name"
className="border border-gray-300 rounded-md p-2 block mb-2"
/>
<input
type="text"
name="price"
placeholder="price"
className="border border-gray-300 rounded-md p-2 block"
/>
<button className="bg-blue-500 text-white rounded-md p-2 mt-2 w-full">
Create Product
</button>
</form>
</div>
);
}
Cuando un server action se ejecuta del lado cliente este hace una peticion POST, por lo que si quieren verlo pueden colocar esta opcion en el archivo next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
logging: {
fetches: true,
}
};
export default nextConfig;
Ademas de revisar la pestaña Network del navegador.
Asi que lo que podemos hacer es usar el hook de react useTransition para poder manejar esto y colocar un estado de Pendiente mientras la respuesta de la peticion POST va llegando.
"use client";
import { saveProduct } from "@/actions/serveActions";
import { useTransition } from "react";
export default function App() {
const [isPending, startTransition] = useTransition();
return (
<div className="flex flex-col items-center justify-center h-screen">
<form action={(e) => startTransition(() => saveProduct(e))}>
<input
type="text"
name="name"
placeholder="name"
className="border border-gray-300 rounded-md p-2 block mb-2"
/>
<input
type="text"
name="price"
placeholder="price"
className="border border-gray-300 rounded-md p-2 block"
/>
<button className="bg-blue-500 text-white rounded-md p-2 mt-2 w-full">
{isPending ? "Creating Product..." : "Create Product"}
</button>
</form>
</div>
);
}
o usar cualquier hook que queramos en realidad
Aqui usamos un useTransition porque este hace una peticion por POST y esta puede tomar tiempo asi que es bueno colocar