Formularios y Mutaciones

Los formularios te permiten crear y actualizar datos en aplicaciones web. Next.js ofrece una forma poderosa de manejar envíos de formularios y mutaciones de datos usando Acciones de Servidor (Server Actions).

Ejemplos

Cómo Funcionan las Acciones de Servidor

Con las Acciones de Servidor, no necesitas crear manualmente endpoints de API. En su lugar, defines funciones asíncronas en el servidor que pueden ser llamadas directamente desde tus componentes.

🎥 Mira: Aprende más sobre formularios y mutaciones con el Enrutador de Aplicación → YouTube (10 minutos).

Las Acciones de Servidor pueden definirse en Componentes de Servidor o llamarse desde Componentes de Cliente. Definir la acción en un Componente de Servidor permite que el formulario funcione sin JavaScript, proporcionando mejora progresiva.

Habilita las Acciones de Servidor en tu archivo next.config.js:

next.config.js
module.exports = {
  experimental: {
    serverActions: true,
  },
}

Es bueno saber:

  • Los formularios que llaman Acciones de Servidor desde Componentes de Servidor pueden funcionar sin JavaScript.
  • Los formularios que llaman Acciones de Servidor desde Componentes de Cliente encolarán los envíos si JavaScript no está cargado aún, priorizando la hidratación del cliente.
  • Las Acciones de Servidor heredan el entorno de ejecución (runtime) de la página o layout donde se usan.
  • Las Acciones de Servidor funcionan con rutas completamente estáticas (incluyendo revalidación de datos con ISR).

Revalidando Datos en Caché

Las Acciones de Servidor se integran profundamente con la arquitectura de caché y revalidación de Next.js. Cuando se envía un formulario, la Acción de Servidor puede actualizar datos en caché y revalidar cualquier clave de caché que deba cambiar.

A diferencia de las aplicaciones tradicionales limitadas a un solo formulario por ruta, las Acciones de Servidor permiten tener múltiples acciones por ruta. Además, el navegador no necesita refrescarse al enviar un formulario. En un solo viaje de red, Next.js puede devolver tanto la UI actualizada como los datos refrescados.

Consulta los ejemplos a continuación para revalidar datos desde Acciones de Servidor.

Ejemplos

Formularios Exclusivos del Servidor

Para crear un formulario exclusivo del servidor, define la Acción de Servidor en un Componente de Servidor. La acción puede definirse en línea con la directiva "use server" al inicio de la función, o en un archivo separado con la directiva al inicio del archivo.

export default function Page() {
  async function create(formData: FormData) {
    'use server'

    // mutar datos
    // revalidar caché
  }

  return <form action={create}>...</form>
}
export default function Page() {
  async function create(formData) {
    'use server'

    // mutar datos
    // revalidar caché
  }

  return <form action={create}>...</form>
}

Es bueno saber: <form action={create}> toma el tipo de dato FormData. En el ejemplo anterior, el FormData enviado a través del form HTML es accesible en la acción de servidor create.

Revalidando Datos

Las Acciones de Servidor te permiten invalidar la Caché de Next.js bajo demanda. Puedes invalidar un segmento de ruta completo con revalidatePath:

'use server'

import { revalidatePath } from 'next/cache'

export default async function submit() {
  await submitForm()
  revalidatePath('/')
}
'use server'

import { revalidatePath } from 'next/cache'

export default async function submit() {
  await submitForm()
  revalidatePath('/')
}

O invalidar una obtención de datos específica con una etiqueta de caché usando revalidateTag:

'use server'

import { revalidateTag } from 'next/cache'

export default async function submit() {
  await addPost()
  revalidateTag('posts')
}
'use server'

import { revalidateTag } from 'next/cache'

export default async function submit() {
  await addPost()
  revalidateTag('posts')
}

Redireccionando

Si deseas redirigir al usuario a una ruta diferente después de completar una Acción de Servidor, puedes usar redirect y cualquier URL absoluta o relativa:

'use server'

import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'

export default async function submit() {
  const id = await addPost()
  revalidateTag('posts') // Actualizar posts en caché
  redirect(`/post/${id}`) // Navegar a nueva ruta
}
'use server'

import { redirect } from 'next/navigation'
import { revalidateTag } from 'next/cache'

export default async function submit() {
  const id = await addPost()
  revalidateTag('posts') // Actualizar posts en caché
  redirect(`/post/${id}`) // Navegar a nueva ruta
}

Validación de Formularios

Recomendamos usar validación HTML como required y type="email" para validación básica de formularios.

Para validación más avanzada en el servidor, usa una biblioteca de validación de esquemas como zod para validar la estructura de los datos del formulario parseados:

import { z } from 'zod'

const schema = z.object({
  // ...
})

export default async function submit(formData: FormData) {
  const parsed = schema.parse({
    id: formData.get('id'),
  })
  // ...
}
import { z } from 'zod'

const schema = z.object({
  // ...
})

export default async function submit(formData) {
  const parsed = schema.parse({
    id: formData.get('id'),
  })
  // ...
}

Mostrando Estado de Carga

Usa el hook useFormStatus para mostrar un estado de carga cuando un formulario se está enviando al servidor. El hook useFormStatus solo puede usarse como hijo de un elemento form que usa una Acción de Servidor.

Por ejemplo, el siguiente botón de envío:

'use client'

import { experimental_useFormStatus as useFormStatus } from 'react-dom'

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

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

import { experimental_useFormStatus as useFormStatus } from 'react-dom'

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

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

<SubmitButton /> puede usarse entonces en un formulario con una Acción de Servidor:

import { SubmitButton } from '@/app/submit-button'

export default async function Home() {
  return (
    <form action={...}>
      <input type="text" name="field-name" />
      <SubmitButton />
    </form>
  )
}
import { SubmitButton } from '@/app/submit-button'

export default async function Home() {
  return (
    <form action={...}>
      <input type="text" name="field-name" />
      <SubmitButton />
    </form>
  )
}

Manejo de Errores

Las Acciones de Servidor (Server Actions) también pueden devolver objetos serializables. Por ejemplo, tu Acción de Servidor podría manejar errores al crear un nuevo elemento:

'use server'

export async function createTodo(prevState: any, formData: FormData) {
  try {
    await createItem(formData.get('todo'))
    return revalidatePath('/')
  } catch (e) {
    return { message: 'Failed to create' }
  }
}
'use server'

export async function createTodo(prevState, formData) {
  try {
    await createItem(formData.get('todo'))
    return revalidatePath('/')
  } catch (e) {
    return { message: 'Failed to create' }
  }
}

Luego, desde un Componente de Cliente (Client Component), puedes leer este valor y mostrar un mensaje de error.

'use client'

import { experimental_useFormState as useFormState } from 'react-dom'
import { experimental_useFormStatus as useFormStatus } from 'react-dom'
import { createTodo } from '@/app/actions'

const initialState = {
  message: null,
}

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

  return (
    <button type="submit" aria-disabled={pending}>
      Add
    </button>
  )
}

export function AddForm() {
  const [state, formAction] = useFormState(createTodo, initialState)

  return (
    <form action={formAction}>
      <label htmlFor="todo">Enter Task</label>
      <input type="text" id="todo" name="todo" required />
      <SubmitButton />
      <p aria-live="polite" className="sr-only">
        {state?.message}
      </p>
    </form>
  )
}
'use client'

import { experimental_useFormState as useFormState } from 'react-dom'
import { experimental_useFormStatus as useFormStatus } from 'react-dom'
import { createTodo } from '@/app/actions'

const initialState = {
  message: null,
}

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

  return (
    <button type="submit" aria-disabled={pending}>
      Add
    </button>
  )
}

export function AddForm() {
  const [state, formAction] = useFormState(createTodo, initialState)

  return (
    <form action={formAction}>
      <label htmlFor="todo">Enter Task</label>
      <input type="text" id="todo" name="todo" required />
      <SubmitButton />
      <p aria-live="polite" className="sr-only">
        {state?.message}
      </p>
    </form>
  )
}

Actualizaciones Optimistas

Usa useOptimistic para actualizar la interfaz de usuario de manera optimista antes de que finalice la Acción de Servidor, en lugar de esperar la respuesta:

'use client'

import { experimental_useOptimistic as useOptimistic } from 'react'
import { send } from './actions'

type Message = {
  message: string
}

export function Thread({ messages }: { messages: Message[] }) {
  const [optimisticMessages, addOptimisticMessage] = useOptimistic<Message[]>(
    messages,
    (state: Message[], newMessage: string) => [
      ...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">Send</button>
      </form>
    </div>
  )
}
'use client'

import { experimental_useOptimistic as 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">Send</button>
      </form>
    </div>
  )
}

Configuración de Cookies

Puedes configurar cookies dentro de una Acción de Servidor usando la función cookies:

'use server'

import { cookies } from 'next/headers'

export async function create() {
  const cart = await createCart()
  cookies().set('cartId', cart.id)
}
'use server'

import { cookies } from 'next/headers'

export async function create() {
  const cart = await createCart()
  cookies().set('cartId', cart.id)
}

Lectura de Cookies

Puedes leer cookies dentro de una Acción de Servidor usando la función cookies:

'use server'

import { cookies } from 'next/headers'

export async function read() {
  const auth = cookies().get('authorization')?.value
  // ...
}
'use server'

import { cookies } from 'next/headers'

export async function read() {
  const auth = cookies().get('authorization')?.value
  // ...
}

Eliminación de Cookies

Puedes eliminar cookies dentro de una Acción de Servidor usando la función cookies:

'use server'

import { cookies } from 'next/headers'

export async function delete() {
  cookies().delete('name')
  // ...
}
'use server'

import { cookies } from 'next/headers'

export async function delete() {
  cookies().delete('name')
  // ...
}

Consulta ejemplos adicionales para eliminar cookies desde Acciones de Servidor.