Acciones de servidor (Server Actions) y mutaciones
Las Acciones de Servidor (Server Actions) son funciones asíncronas que se ejecutan en el servidor. Pueden usarse en Componentes de Servidor y Cliente para manejar envíos de formularios y mutaciones de datos en aplicaciones Next.js.
🎥 Ver: Aprende más sobre formularios y mutaciones con Acciones de Servidor → YouTube (10 minutos).
Convención
Una Acción de Servidor puede definirse con la directiva "use server"
de React. Puedes colocar la directiva al inicio de una función async
para marcarla como Acción de Servidor, o al inicio de un archivo separado para marcar todas sus exportaciones como Acciones de Servidor.
Componentes de Servidor
Los Componentes de Servidor pueden usar la directiva "use server"
a nivel de función o módulo. Para definir una Acción de Servidor inline, agrega "use server"
al inicio del cuerpo de la función:
// Componente de Servidor
export default function Page() {
// Acción de Servidor
async function create() {
'use server'
// ...
}
return (
// ...
)
}
// Componente de Servidor
export default function Page() {
// Acción de Servidor
async function create() {
'use server'
// ...
}
return (
// ...
)
}
Componentes de Cliente
Los Componentes de Cliente solo pueden importar acciones que usen la directiva "use server"
a nivel de módulo.
Para llamar una Acción de Servidor en un Componente de Cliente, crea un nuevo archivo y agrega la directiva "use server"
al inicio. Todas las funciones dentro del archivo serán marcadas como Acciones de Servidor que pueden reutilizarse en Componentes de Cliente y Servidor:
'use server'
export async function create() {
// ...
}
'use server'
export async function create() {
// ...
}
También puedes pasar una Acción de Servidor a un Componente de Cliente como prop:
<ClientComponent updateItem={updateItem} />
'use client'
export default function ClientComponent({ updateItem }) {
return <form action={updateItem}>{/* ... */}</form>
}
Comportamiento
- Las Acciones de Servidor pueden invocarse usando el atributo
action
en un elemento<form>
:- Los Componentes de Servidor soportan mejora progresiva por defecto, lo que significa que el formulario se enviará incluso si JavaScript no se ha cargado o está deshabilitado.
- En Componentes de Cliente, los formularios que invocan Acciones de Servidor encolarán los envíos si JavaScript no está cargado aún, priorizando la hidratación del cliente.
- Después de la hidratación, el navegador no se recarga al enviar el formulario.
- Las Acciones de Servidor no están limitadas a
<form>
y pueden invocarse desde manejadores de eventos,useEffect
, bibliotecas de terceros y otros elementos como<button>
. - Las Acciones de Servidor se integran con la arquitectura de caché y revalidación de Next.js. Cuando se invoca una acción, Next.js puede devolver tanto la UI actualizada como nuevos datos en un solo viaje al servidor.
- Internamente, las acciones usan el método
POST
, y solo este método HTTP puede invocarlas. - Los argumentos y valores de retorno de las Acciones de Servidor deben ser serializables por React. Consulta la documentación de React para ver una lista de argumentos y valores serializables.
- Las Acciones de Servidor son funciones. Esto significa que pueden reutilizarse en cualquier parte de tu aplicación.
- Las Acciones de Servidor heredan el entorno de ejecución de la página o layout donde se usan.
- Las Acciones de Servidor heredan la Configuración del Segmento de Ruta de la página o layout donde se usan, incluyendo campos como
maxDuration
.
Ejemplos
Formularios
React extiende el elemento HTML <form>
para permitir invocar Acciones de Servidor con la prop action
.
Cuando se invoca en un formulario, la acción recibe automáticamente el objeto FormData
. No necesitas usar useState
de React para manejar campos, en su lugar puedes extraer los datos usando los métodos nativos de FormData
:
export default function Page() {
async function createInvoice(formData: FormData) {
'use server'
const rawFormData = {
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
}
// mutar datos
// revalidar caché
}
return <form action={createInvoice}>...</form>
}
export default function Page() {
async function createInvoice(formData) {
'use server'
const rawFormData = {
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
}
// mutar datos
// revalidar caché
}
return <form action={createInvoice}>...</form>
}
Nota importante:
- Ejemplo: Formulario con Estados de Carga y Error
- Al trabajar con formularios que tienen muchos campos, puedes considerar usar el método
entries()
conObject.fromEntries()
de JavaScript. Por ejemplo:const rawFormData = Object.fromEntries(formData)
. Ten en cuenta que elformData
incluirá propiedades adicionales$ACTION_
.- Consulta la documentación de React
<form>
para aprender más.
Pasando argumentos adicionales
Puedes pasar argumentos adicionales a una Acción de Servidor usando el método bind
de JavaScript.
'use client'
import { updateUser } from './actions'
export function UserProfile({ userId }: { userId: string }) {
const updateUserWithId = updateUser.bind(null, userId)
return (
<form action={updateUserWithId}>
<input type="text" name="name" />
<button type="submit">Actualizar Nombre</button>
</form>
)
}
'use client'
import { updateUser } from './actions'
export function UserProfile({ userId }) {
const updateUserWithId = updateUser.bind(null, userId)
return (
<form action={updateUserWithId}>
<input type="text" name="name" />
<button type="submit">Actualizar Nombre</button>
</form>
)
}
La Acción de Servidor recibirá el argumento userId
, además de los datos del formulario:
'use server'
export async function updateUser(userId, formData) {
// ...
}
Nota importante:
- Una alternativa es pasar argumentos como campos ocultos en el formulario (ej.
<input type="hidden" name="userId" value={userId} />
). Sin embargo, el valor será parte del HTML renderizado y no estará codificado..bind
funciona tanto en Componentes de Servidor como de Cliente. También soporta mejora progresiva.
Estados pendientes
Puedes usar el hook useFormStatus
de React para mostrar un estado pendiente mientras se envía el formulario.
useFormStatus
devuelve el estado para un<form>
específico, por lo que debe definirse como hijo del elemento<form>
.useFormStatus
es un hook de React y por lo tanto debe usarse en un Componente de Cliente.
<SubmitButton />
puede luego anidarse en cualquier formulario:
import { SubmitButton } from '@/app/submit-button'
import { createItem } from '@/app/actions'
// Componente de Servidor
export default async function Home() {
return (
<form action={createItem}>
<input type="text" name="field-name" />
<SubmitButton />
</form>
)
}
import { SubmitButton } from '@/app/submit-button'
import { createItem } from '@/app/actions'
// Componente de Servidor
export default async function Home() {
return (
<form action={createItem}>
<input type="text" name="field-name" />
<SubmitButton />
</form>
)
}
Validación en servidor y manejo de errores
Recomendamos usar validación HTML como required
y type="email"
para validación básica en cliente.
Para validación más avanzada en servidor, puedes usar una biblioteca como zod para validar los campos del formulario antes de mutar los datos:
'use server'
import { z } from 'zod'
const schema = z.object({
email: z.string({
invalid_type_error: 'Email inválido',
}),
})
export default async function createUser(formData: FormData) {
const validatedFields = schema.safeParse({
email: formData.get('email'),
})
// Retornar temprano si los datos son inválidos
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
// Mutar datos
}
'use server'
import { z } from 'zod'
const schema = z.object({
email: z.string({
invalid_type_error: 'Email inválido',
}),
})
export default async function createsUser(formData) {
const validatedFields = schema.safeParse({
email: formData.get('email'),
})
// Retornar temprano si los datos son inválidos
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
}
}
// Mutar datos
}
Una vez validados los campos en el servidor, puedes retornar un objeto serializable en tu acción y usar el hook useFormState
de React para mostrar un mensaje al usuario.
- Al pasar la acción a
useFormState
, la firma de la función cambia para recibir un nuevo parámetroprevState
oinitialState
como primer argumento. useFormState
es un hook de React y por lo tanto debe usarse en un Componente de Cliente.
'use server'
export async function createUser(prevState: any, formData: FormData) {
// ...
return {
message: 'Por favor ingresa un email válido',
}
}
'use server'
export async function createUser(prevState, formData) {
// ...
return {
message: 'Por favor ingresa un email válido',
}
}
Luego, puedes pasar tu acción al hook useFormState
y usar el state
devuelto para mostrar un mensaje de error.
'use client'
import { useFormState } from 'react-dom'
import { createUser } from '@/app/actions'
const initialState = {
message: '',
}
export function Signup() {
const [state, formAction] = useFormState(createUser, initialState)
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite" className="sr-only">
{state?.message}
</p>
<button>Registrarse</button>
</form>
)
}
'use client'
import { useFormState } from 'react-dom'
import { createUser } from '@/app/actions'
const initialState = {
message: '',
}
export function Signup() {
const [state, formAction] = useFormState(createUser, initialState)
return (
<form action={formAction}>
<label htmlFor="email">Email</label>
<input type="text" id="email" name="email" required />
{/* ... */}
<p aria-live="polite" className="sr-only">
{state?.message}
</p>
<button>Registrarse</button>
</form>
)
}
Nota importante:
- Antes de mutar datos, siempre debes asegurarte de que el usuario esté autorizado para realizar la acción. Ver Autenticación y Autorización.
Actualizaciones optimistas
Puedes usar el hook useOptimistic
de React para actualizar la UI de manera optimista antes de que la Acción de Servidor termine, en lugar de esperar la respuesta:
'use client'
import { useOptimistic } from 'react'
import { send } from './actions'
type Message = {
message: string
}
export function Thread({ messages }: { messages: Message[] }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic<
Message[],
string
>(messages, (state, newMessage) => [...state, { message: newMessage }])
return (
<div>
{optimisticMessages.map((m, k) => (
<div key={k}>{m.message}</div>
))}
<form
action={async (formData: FormData) => {
const message = formData.get('message')
addOptimisticMessage(message)
await send(message)
}}
>
<input type="text" name="message" />
<button type="submit">Enviar</button>
</form>
</div>
)
}
'use client'
import { useOptimistic } from 'react'
import { send } from './actions'
export function Thread({ messages }) {
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages,
(state, newMessage) => [...state, { message: newMessage }]
)
return (
<div>
{optimisticMessages.map((m) => (
<div>{m.message}</div>
))}
<form
action={async (formData) => {
const message = formData.get('message')
addOptimisticMessage(message)
await send(message)
}}
>
<input type="text" name="message" />
<button type="submit">Enviar</button>
</form>
</div>
)
}
Elementos anidados
Puedes invocar una Acción de Servidor en elementos anidados dentro de <form>
como <button>
, <input type="submit">
y <input type="image">
. Estos elementos aceptan la prop formAction
o manejadores de eventos.
Esto es útil cuando quieres llamar múltiples acciones de servidor dentro de un formulario. Por ejemplo, puedes crear un elemento <button>
específico para guardar un borrador de publicación además de publicarlo. Consulta la documentación de React <form>
para más información.
Envío programático de formularios
Puede activar el envío de un formulario utilizando el método requestSubmit()
. Por ejemplo, cuando el usuario presiona ⌘
+ Enter
, puede escuchar el evento onKeyDown
:
'use client'
export function Entry() {
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (
(e.ctrlKey || e.metaKey) &&
(e.key === 'Enter' || e.key === 'NumpadEnter')
) {
e.preventDefault()
e.currentTarget.form?.requestSubmit()
}
}
return (
<div>
<textarea name="entry" rows={20} required onKeyDown={handleKeyDown} />
</div>
)
}
'use client'
export function Entry() {
const handleKeyDown = (e) => {
if (
(e.ctrlKey || e.metaKey) &&
(e.key === 'Enter' || e.key === 'NumpadEnter')
) {
e.preventDefault()
e.currentTarget.form?.requestSubmit()
}
}
return (
<div>
<textarea name="entry" rows={20} required onKeyDown={handleKeyDown} />
</div>
)
}
Esto activará el envío del ancestro <form>
más cercano, lo que invocará la Acción del Servidor (Server Action).
Elementos que no son formularios
Aunque es común usar Acciones del Servidor dentro de elementos <form>
, también se pueden invocar desde otras partes de su código, como manejadores de eventos y useEffect
.
Manejadores de eventos
Puede invocar una Acción del Servidor desde manejadores de eventos como onClick
. Por ejemplo, para incrementar un contador de "me gusta":
Para mejorar la experiencia del usuario, recomendamos usar otras APIs de React como useOptimistic
y useTransition
para actualizar la interfaz de usuario antes de que la Acción del Servidor termine de ejecutarse en el servidor, o para mostrar un estado de carga.
También puede agregar manejadores de eventos a elementos de formulario, por ejemplo, para guardar un campo de formulario onChange
:
'use client'
import { publishPost, saveDraft } from './actions'
export default function EditPost() {
return (
<form action={publishPost}>
<textarea
name="content"
onChange={async (e) => {
await saveDraft(e.target.value)
}}
/>
<button type="submit">Publicar</button>
</form>
)
}
Para casos como este, donde se pueden activar múltiples eventos en rápida sucesión, recomendamos debouncing para evitar invocaciones innecesarias de Acciones del Servidor.
useEffect
Puede usar el hook de React useEffect
para invocar una Acción del Servidor cuando el componente se monta o cambia una dependencia. Esto es útil para mutaciones que dependen de eventos globales o que necesitan activarse automáticamente. Por ejemplo, onKeyDown
para atajos de teclado, un hook de observador de intersección para scroll infinito, o cuando el componente se monta para actualizar un contador de visitas:
'use client'
import { incrementViews } from './actions'
import { useState, useEffect } from 'react'
export default function ViewCount({ initialViews }: { initialViews: number }) {
const [views, setViews] = useState(initialViews)
useEffect(() => {
const updateViews = async () => {
const updatedViews = await incrementViews()
setViews(updatedViews)
}
updateViews()
}, [])
return <p>Total de visitas: {views}</p>
}
'use client'
import { incrementViews } from './actions'
import { useState, useEffect } from 'react'
export default function ViewCount({ initialViews }: { initialViews: number }) {
const [views, setViews] = useState(initialViews)
useEffect(() => {
const updateViews = async () => {
const updatedViews = await incrementViews()
setViews(updatedViews)
}
updateViews()
}, [])
return <p>Total de visitas: {views}</p>
}
Recuerde considerar el comportamiento y advertencias de useEffect
.
Manejo de errores
Cuando se lanza un error, será capturado por el límite error.js
más cercano o <Suspense>
en el cliente. Recomendamos usar try/catch
para devolver errores que puedan ser manejados por su interfaz de usuario.
Por ejemplo, su Acción del Servidor podría manejar errores al crear un nuevo elemento devolviendo un mensaje:
'use server'
export async function createTodo(prevState: any, formData: FormData) {
try {
// Mutar datos
} catch (e) {
throw new Error('Error al crear la tarea')
}
}
'use server'
export async function createTodo(prevState, formData) {
try {
// Mutar datos
} catch (e) {
throw new Error('Error al crear la tarea')
}
}
Nota importante:
- Además de lanzar el error, también puede devolver un objeto para ser manejado por
useFormState
. Consulte Validación y manejo de errores en el servidor.
Revalidación de datos
Puede revalidar la Caché de Next.js dentro de sus Acciones del Servidor con la API revalidatePath
:
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost() {
try {
// ...
} catch (error) {
// ...
}
revalidatePath('/posts')
}
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost() {
try {
// ...
} catch (error) {
// ...
}
revalidatePath('/posts')
}
O invalidar una obtención de datos específica con una etiqueta de caché usando revalidateTag
:
'use server'
import { revalidateTag } from 'next/cache'
export async function createPost() {
try {
// ...
} catch (error) {
// ...
}
revalidateTag('posts')
}
'use server'
import { revalidateTag } from 'next/cache'
export async function createPost() {
try {
// ...
} catch (error) {
// ...
}
revalidateTag('posts')
}
Redireccionamiento
Si desea redirigir al usuario a una ruta diferente después de completar una Acción del Servidor, puede usar la API redirect
. redirect
debe llamarse fuera del bloque try/catch
:
'use server'
import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'
export async function createPost(id: string) {
try {
// ...
} catch (error) {
// ...
}
revalidateTag('posts') // Actualizar posts en caché
redirect(`/post/${id}`) // Navegar a la página del nuevo post
}
'use server'
import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'
export async function createPost(id) {
try {
// ...
} catch (error) {
// ...
}
revalidateTag('posts') // Actualizar posts en caché
redirect(`/post/${id}`) // Navegar a la página del nuevo post
}
Cookies
Puede obtener
, establecer
y eliminar
cookies dentro de una Acción del Servidor usando la API cookies
:
'use server'
import { cookies } from 'next/headers'
export async function exampleAction() {
// Obtener cookie
const value = cookies().get('name')?.value
// Establecer cookie
cookies().set('name', 'Delba')
// Eliminar cookie
cookies().delete('name')
}
'use server'
import { cookies } from 'next/headers'
export async function exampleAction() {
// Obtener cookie
const value = cookies().get('name')?.value
// Establecer cookie
cookies().set('name', 'Delba')
// Eliminar cookie
cookies().delete('name')
}
Consulte ejemplos adicionales para eliminar cookies desde Acciones del Servidor.
Seguridad
Autenticación y autorización
Debe tratar las Acciones del Servidor como lo haría con puntos finales de API públicos y asegurarse de que el usuario esté autorizado para realizar la acción. Por ejemplo:
'use server'
import { auth } from './lib'
export function addItem() {
const { user } = auth()
if (!user) {
throw new Error('Debes iniciar sesión para realizar esta acción')
}
// ...
}
Cierres y encriptación
Definir una Acción del Servidor dentro de un componente crea un cierre donde la acción tiene acceso al alcance de la función externa. Por ejemplo, la acción publish
tiene acceso a la variable publishVersion
:
export default function Page() {
const publishVersion = await getLatestVersion();
async function publish(formData: FormData) {
"use server";
if (publishVersion !== await getLatestVersion()) {
throw new Error('La versión ha cambiado desde que se presionó publicar');
}
...
}
return <button action={publish}>Publicar</button>;
}
export default function Page() {
const publishVersion = await getLatestVersion();
async function publish() {
"use server";
if (publishVersion !== await getLatestVersion()) {
throw new Error('La versión ha cambiado desde que se presionó publicar');
}
...
}
return <button action={publish}>Publicar</button>;
}
Los cierres son útiles cuando necesita capturar una instantánea de datos (por ejemplo, publishVersion
) en el momento de la representación para que pueda usarse más tarde cuando se invoque la acción.
Sin embargo, para que esto suceda, las variables capturadas se envían al cliente y de vuelta al servidor cuando se invoca la acción. Para evitar que los datos sensibles se expongan al cliente, Next.js encripta automáticamente las variables cerradas. Se genera una nueva clave privada para cada acción cada vez que se construye una aplicación Next.js. Esto significa que las acciones solo se pueden invocar para una compilación específica.
Nota importante: No recomendamos confiar únicamente en la encriptación para evitar que los valores sensibles se expongan en el cliente. En su lugar, debe usar las APIs de taint de React para evitar proactivamente que datos específicos se envíen al cliente.
Sobrescribir claves de encriptación (avanzado)
Cuando aloja su aplicación Next.js en múltiples servidores, cada instancia del servidor puede terminar con una clave de encriptación diferente, lo que puede llevar a inconsistencias.
Para mitigar esto, puede sobrescribir la clave de encriptación usando la variable de entorno process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY
. Especificar esta variable asegura que sus claves de encriptación sean persistentes entre compilaciones y que todas las instancias del servidor usen la misma clave.
Este es un caso de uso avanzado donde el comportamiento de encriptación consistente en múltiples implementaciones es crítico para su aplicación. Debe considerar prácticas de seguridad estándar como la rotación de claves y la firma.
Nota importante: Las aplicaciones Next.js implementadas en Vercel manejan esto automáticamente.
Orígenes permitidos (avanzado)
Dado que las Acciones del Servidor se pueden invocar en un elemento <form>
, esto las expone a ataques CSRF.
Internamente, las Acciones del Servidor usan el método POST
, y solo se permite este método HTTP para invocarlas. Esto evita la mayoría de las vulnerabilidades CSRF en navegadores modernos, especialmente con las cookies SameSite siendo el valor predeterminado.
Como protección adicional, las Acciones del Servidor en Next.js también comparan la cabecera Origin con la cabecera Host (o X-Forwarded-Host
). Si no coinciden, la solicitud se abortará. En otras palabras, las Acciones del Servidor solo se pueden invocar en el mismo host que la página que las aloja.
Para aplicaciones grandes que usan proxies inversos o arquitecturas de backend multicapa (donde la API del servidor difiere del dominio de producción), se recomienda usar la opción de configuración serverActions.allowedOrigins
para especificar una lista de orígenes seguros. La opción acepta un array de strings.
/** @type {import('next').NextConfig} */
module.exports = {
experimental: {
serverActions: {
allowedOrigins: ['my-proxy.com', '*.my-proxy.com'],
},
},
}
Aprenda más sobre Seguridad y Acciones del Servidor.
Recursos adicionales
Para más información sobre Acciones del Servidor, consulte los siguientes documentos de React: