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() {
  // ...
}
import { create } from '@/app/actions'

export function Button() {
  return (
    // ...
  )
}
import { create } from '@/app/actions'

export function Button() {
  return (
    // ...
  )
}

También puedes pasar una Acción de Servidor a un Componente de Cliente como prop:

<ClientComponent updateItem={updateItem} />
app/client-component.jsx
'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:

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:

app/actions.js
'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.
'use client'

import { useFormStatus } from 'react-dom'

export function SubmitButton() {
  const { pending } = useFormStatus()

  return (
    <button type="submit" disabled={pending}>
      Agregar
    </button>
  )
}
'use client'

import { useFormStatus } from 'react-dom'

export function SubmitButton() {
  const { pending } = useFormStatus()

  return (
    <button type="submit" disabled={pending}>
      Agregar
    </button>
  )
}

<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ámetro prevState o initialState 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:

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":

'use client'

import { incrementLike } from './actions'
import { useState } from 'react'

export default function LikeButton({ initialLikes }: { initialLikes: number }) {
  const [likes, setLikes] = useState(initialLikes)

  return (
    <>
      <p>Total de "Me gusta": {likes}</p>
      <button
        onClick={async () => {
          const updatedLikes = await incrementLike()
          setLikes(updatedLikes)
        }}
      >
        Me gusta
      </button>
    </>
  )
}
'use client'

import { incrementLike } from './actions'
import { useState } from 'react'

export default function LikeButton({ initialLikes }) {
  const [likes, setLikes] = useState(initialLikes)

  return (
    <>
      <p>Total de "Me gusta": {likes}</p>
      <button
        onClick={async () => {
          const updatedLikes = await incrementLike()
          setLikes(updatedLikes)
        }}
      >
        Me gusta
      </button>
    </>
  )
}

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:

app/ui/edit-post.tsx
'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:

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:

app/actions.ts
'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.

next.config.js
/** @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: