ngnext

Formulários

Angular tem dois sistemas robustos de formulários embutidos. No Next.js, você tem várias opções — desde Server Actions (sem JavaScript no cliente) até React Hook Form para formulários complexos com validação client-side.

Reactive Forms vs. Server Actions

A abordagem recomendada no Next.js para formulários simples são as Server Actions — funções que rodam no servidor diretamente chamadas pelo atributo action do form:

Angularlogin.component.ts
// Reactive Forms — Angular
@Component({
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()">
      <input formControlName="email" type="email" />
      <span *ngIf="form.get('email')?.errors?.['required']">
        E-mail obrigatório
      </span>
      <span *ngIf="form.get('email')?.errors?.['email']">
        E-mail inválido
      </span>

      <input formControlName="password" type="password" />
      <span *ngIf="form.get('password')?.errors?.['minlength']">
        Mínimo 8 caracteres
      </span>

      <button type="submit" [disabled]="form.invalid">
        Entrar
      </button>
    </form>
  `,
})
export class LoginComponent {
  form = this.fb.group({
    email: ['', [Validators.required, Validators.email]],
    password: ['', [
      Validators.required,
      Validators.minLength(8),
    ]],
  });

  constructor(private fb: FormBuilder) {}

  onSubmit() {
    if (this.form.valid) {
      console.log(this.form.value);
    }
  }
}
Next.jsapp/login/page.tsx
// Server Action + formulário HTML — Next.js
// Roda no servidor: sem JS no cliente necessário!

async function login(formData: FormData) {
  "use server"; // esta função roda no servidor

  const email = formData.get("email") as string;
  const password = formData.get("password") as string;

  // Validação no servidor
  if (!email || !password) {
    throw new Error("Campos obrigatórios");
  }

  // Lógica de autenticação diretamente aqui
  await signIn({ email, password });
  redirect("/dashboard");
}

// Server Component — sem useState, sem 'use client'
export default function LoginPage() {
  return (
    <form action={login}>
      <input name="email" type="email" required />
      <input name="password" type="password" minLength={8} required />
      <button type="submit">Entrar</button>
    </form>
  );
}
Server Actions funcionam sem JavaScript habilitado no browser. São progressively enhanced por padrão — o formulário funciona como HTML puro com submit normal, e com JS ativo ganha comportamento de SPA.

Template-driven vs. Controlled Inputs

Os formulários template-driven do Angular têm um equivalente em React usando inputs controlados (controlled inputs), onde você guarda o valor em estado com useState:

Para formulários com validação client-side

Angular
<!-- Template-driven Forms — Angular -->
<form #loginForm="ngForm" (ngSubmit)="onSubmit(loginForm)">
  <input
    name="email"
    type="email"
    ngModel
    required
    email
    #emailField="ngModel"
  />
  <span *ngIf="emailField.invalid && emailField.touched">
    E-mail inválido
  </span>

  <input
    name="password"
    type="password"
    ngModel
    required
    minlength="8"
  />

  <button type="submit" [disabled]="loginForm.invalid">
    Entrar
  </button>
</form>
Next.js
"use client";
import { useState } from "react";

export default function LoginForm() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [errors, setErrors] = useState<Record<string, string>>({});

  function validate() {
    const errs: Record<string, string> = {};
    if (!email) errs.email = "E-mail obrigatório";
    else if (!/S+@S+/.test(email)) errs.email = "E-mail inválido";
    if (password.length < 8) errs.password = "Mínimo 8 caracteres";
    return errs;
  }

  function onSubmit(e: React.FormEvent) {
    e.preventDefault();
    const errs = validate();
    if (Object.keys(errs).length > 0) {
      setErrors(errs);
      return;
    }
    console.log({ email, password });
  }

  return (
    <form onSubmit={onSubmit}>
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
      {errors.email && <span>{errors.email}</span>}
      <input value={password} onChange={(e) => setPassword(e.target.value)} />
      {errors.password && <span>{errors.password}</span>}
      <button type="submit">Entrar</button>
    </form>
  );
}

React Hook Form — equivalente ao FormBuilder

Para formulários mais complexos, a biblioteca React Hook Form oferece a experiência mais próxima do Reactive Forms do Angular, com suporte a Zod e Yup para validação de esquema:

LoginForm.tsx
"use client";
// React Hook Form — equivale ao FormBuilder do Angular
// npm install react-hook-form
import { useForm } from "react-hook-form";

type FormData = {
  email: string;
  password: string;
};

export default function LoginForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<FormData>();

  function onSubmit(data: FormData) {
    console.log(data); // { email: '...', password: '...' }
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        type="email"
        {...register("email", {
          required: "E-mail obrigatório",
          pattern: {
            value: /S+@S+/,
            message: "E-mail inválido",
          },
        })}
      />
      {errors.email && <span>{errors.email.message}</span>}

      <input
        type="password"
        {...register("password", {
          required: "Senha obrigatória",
          minLength: { value: 8, message: "Mínimo 8 caracteres" },
        })}
      />
      {errors.password && <span>{errors.password.message}</span>}

      <button type="submit" disabled={isSubmitting}>
        Entrar
      </button>
    </form>
  );
}

useActionState — feedback em Server Actions

O hook useActionState do React 19 permite gerenciar estado de submissão e mensagens de erro em Server Actions, mantendo a lógica no servidor:

LoginForm.tsx
"use client";
import { useActionState } from "react"; // React 19+

async function login(prevState: string | null, formData: FormData) {
  "use server";
  const email = formData.get("email") as string;
  const password = formData.get("password") as string;

  if (!email || !password) return "Preencha todos os campos";

  const ok = await authenticate(email, password);
  if (!ok) return "Credenciais inválidas";

  redirect("/dashboard");
  return null;
}

export default function LoginForm() {
  const [error, formAction, isPending] = useActionState(login, null);

  return (
    <form action={formAction}>
      <input name="email" type="email" required />
      <input name="password" type="password" required />
      {error && <p className="text-red-500">{error}</p>}
      <button type="submit" disabled={isPending}>
        {isPending ? "Entrando..." : "Entrar"}
      </button>
    </form>
  );
}

Quando usar cada abordagem

AbordagemQuando usarEquivalente Angular
Server Action + form actionFormulários simples: login, contato, cadastroFormulário com POST para API
useActionState + Server ActionQuando precisa de feedback (loading, erros) com lógica serverReactive Forms + service
Controlled inputs + useStateFormulários com validação instantânea client-sideTemplate-driven Forms
React Hook FormFormulários complexos, multi-step, validação com Zod/YupReactive Forms + FormBuilder