Introducción/Guías/Formularios

Cómo crear formularios con Acciones de Servidor (Server Actions)

Las Acciones de Servidor (Server Actions) de React son Funciones del Servidor que se ejecutan en el servidor. Pueden ser llamadas en Componentes del Servidor y del Cliente para manejar envíos de formularios. Esta guía le mostrará cómo crear formularios en Next.js con Acciones de Servidor.

Cómo funciona

React extiende el elemento HTML <form> para permitir invocar Acciones de Servidor mediante el atributo action.

Cuando se usa en un formulario, la función recibe automáticamente el objeto FormData. Luego puede extraer los datos utilizando 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'),
    }

    // mutate data
    // revalidate the cache
  }

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

Nota importante: Cuando trabaje con formularios que tienen múltiples campos, puede usar el método entries() con Object.fromEntries() de JavaScript. Por ejemplo: const rawFormData = Object.fromEntries(formData).

Pasar argumentos adicionales

Fuera de los campos del formulario, puede pasar argumentos adicionales a una Función del Servidor usando el método bind de JavaScript. Por ejemplo, para pasar el argumento userId a la Función del Servidor updateUser:

'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 de usuario</button>
    </form>
  )
}

La Función del Servidor recibirá el userId como argumento adicional:

'use server'

export async function updateUser(userId: string, formData: FormData) {}

Nota importante:

  • Una alternativa es pasar los 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 del Servidor como del Cliente y soporta mejora progresiva.

Validación de formularios

Los formularios pueden validarse en el cliente o en el servidor.

  • Para validación del lado del cliente, puede usar atributos HTML como required y type="email" para validación básica.
  • Para validación del lado del servidor, puede usar una biblioteca como zod para validar los campos del formulario. Por ejemplo:
'use server'

import { z } from 'zod'

const schema = z.object({
  email: z.string({
    invalid_type_error: 'Correo electrónico inválido',
  }),
})

export default async function createUser(formData: FormData) {
  const validatedFields = schema.safeParse({
    email: formData.get('email'),
  })

  // Retornar temprano si los datos del formulario son inválidos
  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
    }
  }

  // Mutar datos
}

Errores de validación

Para mostrar errores o mensajes de validación, convierta el componente que define el <form> en un Componente del Cliente y use el hook useActionState de React.

Al usar useActionState, la firma de la función del Servidor cambiará para recibir un nuevo parámetro prevState o initialState como primer argumento.

'use server'

import { z } from 'zod'

export async function createUser(initialState: any, formData: FormData) {
  const validatedFields = schema.safeParse({
    email: formData.get('email'),
  })
  // ...
}
'use server'

import { z } from 'zod'

// ...

export async function createUser(initialState, formData) {
  const validatedFields = schema.safeParse({
    email: formData.get('email'),
  })
  // ...
}

Luego puede renderizar condicionalmente el mensaje de error basado en el objeto state.

'use client'

import { useActionState } from 'react'
import { createUser } from '@/app/actions'

const initialState = {
  message: '',
}

export function Signup() {
  const [state, formAction, pending] = useActionState(createUser, initialState)

  return (
    <form action={formAction}>
      <label htmlFor="email">Correo electrónico</label>
      <input type="text" id="email" name="email" required />
      {/* ... */}
      <p aria-live="polite">{state?.message}</p>
      <button disabled={pending}>Registrarse</button>
    </form>
  )
}

Estados pendientes

El hook useActionState expone un booleano pending que puede usarse para mostrar un indicador de carga o deshabilitar el botón de envío mientras se ejecuta la acción.

'use client'

import { useActionState } from 'react'
import { createUser } from '@/app/actions'

export function Signup() {
  const [state, formAction, pending] = useActionState(createUser, initialState)

  return (
    <form action={formAction}>
      {/* Otros elementos del formulario */}
      <button disabled={pending}>Registrarse</button>
    </form>
  )
}

Alternativamente, puede usar el hook useFormStatus para mostrar un indicador de carga mientras se ejecuta la acción. Al usar este hook, necesitará crear un componente separado para renderizar el indicador de carga. Por ejemplo, para deshabilitar el botón cuando la acción está pendiente:

'use client'

import { useFormStatus } from 'react-dom'

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

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

Luego puede anidar el componente SubmitButton dentro del formulario:

import { SubmitButton } from './button'
import { createUser } from '@/app/actions'

export function Signup() {
  return (
    <form action={createUser}>
      {/* Otros elementos del formulario */}
      <SubmitButton />
    </form>
  )
}

Nota importante: En React 19, useFormStatus incluye claves adicionales en el objeto retornado, como data, method y action. Si no está usando React 19, solo está disponible la clave pending.

Actualizaciones optimistas

Puede usar el hook useOptimistic de React para actualizar optimistamente la UI antes de que la Función del Servidor termine de ejecutarse, 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 }])

  const formAction = async (formData: FormData) => {
    const message = formData.get('message') as string
    addOptimisticMessage(message)
    await send(message)
  }

  return (
    <div>
      {optimisticMessages.map((m, i) => (
        <div key={i}>{m.message}</div>
      ))}
      <form action={formAction}>
        <input type="text" name="message" />
        <button type="submit">Enviar</button>
      </form>
    </div>
  )
}

Elementos anidados de formulario

Puede llamar Acciones 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 en casos donde desea llamar múltiples Acciones de Servidor dentro de un formulario. Por ejemplo, puede crear un elemento <button> específico para guardar un borrador de publicación además de publicarlo. Consulte la documentación de <form> de React para más información.

Envío programático de formularios

Puede activar el envío de un formulario programáticamente usando el método requestSubmit(). Por ejemplo, cuando el usuario envía un formulario usando el atajo de teclado + 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>
  )
}

Esto activará el envío del <form> ancestro más cercano, lo que invocará la Función del Servidor.

On this page