# A form component with a Server Action in the Next.js App Router SDK

> Recipe: make a newsletter form that authors can put on a page. The form sends the data to a Next.js Server Action, shows errors and a success message, and sets a quirk for personalization.

Source: https://docs.uniform.app/docs/sdk/nextjs-app-router/recipes/forms

**Goal:** authors put a newsletter form on a page in Canvas, and set its labels. The visitor types an email address. A Next.js Server Action validates the address and sends it to your email service. After the signup, Uniform Context gets a quirk, so personalizations can change for the visitor.

You will make these files:

| File | Type | Job |
| --- | --- | --- |
| `components/newsletter/types.ts` | Shared | The state of the form. |
| `app/actions/subscribe.ts` | Server Action | Validates the data and sends it to your email service. |
| `components/newsletter/NewsletterFormClient.tsx` | Client component | The form, the errors and the success message. |
| `components/newsletter/NewsletterForm.tsx` | Server component | The Uniform component. It reads the parameters. |

## Prerequisites

- An app with the Uniform SDK and a `resolveComponent` function. Refer to [Map Uniform types to React components](https://docs.uniform.app/docs/sdk/nextjs-app-router/tutorial#step-13-map-uniform-types-to-react-components).
- A component type `newsletterForm` in Uniform, with these text parameters: `title`, `emailLabel`, `buttonLabel` and `successMessage`. Allow the type in the slots where authors can put the form.
- An email service with an HTTP API. The example sends a `POST` request to the URL in the environment variable `NEWSLETTER_API_URL`. Change this part for your service.

## Step 1: Define the form state

The Server Action and the client component share this type:

`components/newsletter/types.ts`

```ts
export type SubscribeState = {
  status: "idle" | "success" | "error";
  message: string;
  email?: string;
};

export const initialSubscribeState: SubscribeState = { status: "idle", message: "" };
```

## Step 2: Make the Server Action

`app/actions/subscribe.ts`

```ts
"use server";

import type { SubscribeState } from "@/components/newsletter/types";

const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

export async function subscribe(_previous: SubscribeState, formData: FormData): Promise<SubscribeState> {
  const email = String(formData.get("email") ?? "").trim();
  const consent = formData.get("consent") === "on";

  // Validate on the server. Do not trust the browser checks.
  if (!EMAIL_PATTERN.test(email)) {
    return { status: "error", message: "Enter a valid email address.", email };
  }
  if (!consent) {
    return { status: "error", message: "Accept the terms to subscribe.", email };
  }

  try {
    // Send the address to your email service.
    const response = await fetch(process.env.NEWSLETTER_API_URL!, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ email }),
      cache: "no-store",
    });
    if (!response.ok) throw new Error(`Status ${response.status}`);
  } catch (error) {
    console.error("Newsletter signup failed", error);
    return { status: "error", message: "We could not subscribe you. Try again later.", email };
  }

  return { status: "success", message: "" };
}
```

- `"use server"` at the top of the file makes each exported async function a Server Action.
- With `useActionState`, the action gets the previous state as the first argument, and the form data as the second argument.
- The action returns the new state. It returns the email address on an error, so the form keeps the value.

> **Warning:**
>
> A Server Action is a public endpoint. Each person who can send a `POST` request to your page can call it. Validate all data on the server. Add rate limits or a bot check before you use the form in production.

## Step 3: Make the client form

`components/newsletter/NewsletterFormClient.tsx`

```tsx
"use client";

import { useActionState, useEffect } from "react";
import { useUniformContext } from "@uniformdev/next-app-router/component";
import { subscribe } from "@/app/actions/subscribe";
import { initialSubscribeState } from "./types";

type Props = {
  emailLabel: string;
  buttonLabel: string;
  successMessage: string;
};

export function NewsletterFormClient({ emailLabel, buttonLabel, successMessage }: Props) {
  const [state, formAction, pending] = useActionState(subscribe, initialSubscribeState);
  const { context } = useUniformContext();

  // Tell Uniform Context that the visitor subscribed.
  useEffect(() => {
    if (state.status === "success") {
      void context?.update({ quirks: { newsletter: "subscribed" } });
    }
  }, [state.status, context]);

  if (state.status === "success") {
    return <p role="status">{successMessage}</p>;
  }

  return (
    <form action={formAction}>
      <label htmlFor="newsletter-email">{emailLabel}</label>
      <input
        id="newsletter-email"
        name="email"
        type="email"
        required
        autoComplete="email"
        defaultValue={state.email}
      />
      <label>
        <input name="consent" type="checkbox" required /> I accept the terms.
      </label>
      <button type="submit" disabled={pending}>
        {pending ? "Sending…" : buttonLabel}
      </button>
      <p aria-live="polite">{state.status === "error" ? state.message : null}</p>
    </form>
  );
}
```

- `useActionState` returns the state, the action for the `<form>`, and `pending`. `pending` is `true` while the action runs.
- `type="email"` and `required` give a fast check in the browser. The server check in Step 2 is the real check.
- `useUniformContext()` returns `{ context }`. `context` is `undefined` until the browser Context is ready. Thus the code uses `context?.`.
- `context.update({ quirks })` gives the quirk `newsletter` the value `subscribed`.

## Step 4: Make the Uniform component

The Uniform component is a server component. It reads the parameters, and gives plain strings to the client form:

`components/newsletter/NewsletterForm.tsx`

```tsx
import {
  type ComponentParameter,
  type ComponentProps,
  UniformText,
} from "@uniformdev/next-app-router/component";
import { NewsletterFormClient } from "./NewsletterFormClient";

type NewsletterFormParameters = {
  title?: ComponentParameter<string>;
  emailLabel?: ComponentParameter<string>;
  buttonLabel?: ComponentParameter<string>;
  successMessage?: ComponentParameter<string>;
};

export const NewsletterForm = ({ parameters, component }: ComponentProps<NewsletterFormParameters>) => (
  <section>
    {parameters.title ? (
      <UniformText component={component} parameter={parameters.title} as="h2" placeholder="Enter a title" />
    ) : null}
    <NewsletterFormClient
      emailLabel={parameters.emailLabel?.value || "Email"}
      buttonLabel={parameters.buttonLabel?.value || "Subscribe"}
      successMessage={parameters.successMessage?.value || "Thank you. You are now subscribed."}
    />
  </section>
);
```

- `UniformText` lets authors edit the title in the page in Canvas.
- The other labels have default values, because a parameter is `undefined` when the author did not fill it in.
- Only serializable props can go from a server component to a client component. Thus the code gives strings, not the parameter objects.

## Step 5: Register the component

Add the component to your `resolveComponent` map:

`components/resolveComponent.tsx`

```tsx
import type { ResolveComponentFunction, ResolveComponentResult } from "@uniformdev/next-app-router";
import type { ComponentProps } from "@uniformdev/next-app-router/component";
import { NewsletterForm } from "./newsletter/NewsletterForm";

const componentMap: Record<string, ResolveComponentResult["component"]> = {
  // ...your other components
  newsletterForm: NewsletterForm,
};

const NotFound = ({ type }: ComponentProps) => <div>Component not found: {type}</div>;

export const resolveComponent: ResolveComponentFunction = ({ component }) => ({
  component: componentMap[component.type] ?? NotFound,
});
```

Then put a `newsletterForm` component on a composition in Canvas, and publish it.

## How it works

1. The page renders the `NewsletterForm` server component with the parameters from Uniform.
2. The browser hydrates `NewsletterFormClient`.
3. The visitor submits the form. React sends a `POST` request to the current URL with the form data.
4. Next.js runs `subscribe` on the server, and sends the new state back to the browser.
5. The client form shows the error, or the success message.
6. On success, the browser Context gets the quirk `newsletter: subscribed`.

The page stays static. The Server Action does not render the page again, because it does not call `revalidatePath` or `refresh`.

## Personalization after the signup

You can use the quirk in Uniform:

- Add the quirk `newsletter` to your Uniform project, and publish the Context manifest.
- Use the quirk in personalization criteria or in visibility rules. For example, show a "Thank you" banner and not the signup form to visitors with `newsletter` = `subscribed`.

When the quirk changes, the page updates in the browser:

- **Lite mode:** the browser chooses all variants, so the page changes at once.
- **Edge mode:** personalizations that the edge rendered also choose again. A/B tests keep their variant.
- **Visibility rules:** the browser applies them with the current quirks.

On the next page load, the quirk comes from a cookie. The browser Context writes the quirks to the cookie `ufvdqk` only when the visitor gave consent, and when `quirkSerialization` is on (the default). The edge middleware reads this cookie. Refer to [Client context](https://docs.uniform.app/docs/sdk/nextjs-app-router/client-context) and [Personalization](https://docs.uniform.app/docs/sdk/nextjs-app-router/personalization).

## Limits

- **Consent:** without consent, the browser Context does not write the quirk to a cookie. The edge and the next full page load do not see it.
- **No data in Uniform:** the form sends the data to your service. Uniform does not store form data.
- **Canvas preview:** a submit in the Canvas preview calls the real Server Action. Use a test address, or skip the request to your service in draft mode.
- **Middleware:** the middleware rewrites the `POST` request of the action, as it does for other requests. Make sure that your middleware matcher includes the page.
