I got tired of writing the same thing over and over for forms in Next.js. So I wrote typed-form-actions.

WILD

Administrator
Staff member
ADMIN
SELLER
SUPREME
MEMBER
Joined
Jan 21, 2025
Messages
220
Reaction score
629
Deposit
0$
I like the direction React and Next.js are heading: native forms, Server Actions, less client-side JavaScript, more progressive enhancement.

It's a beautiful concept.

But as soon as a form becomes more complex than a single email field, it becomes clear that a lot of the same old glue code appears around it:

get values from FormData

collect arrays and nested fields

validate everything via zod

turn zod errors into a convenient object for the UI

return a predictable state for useActionState

manually writing defaultValue, defaultChecked, aria-invalid, aria-describedby again

repeat this in every subsequent form

At some point, I realized I wasn't writing business logic, but rather the same infrastructure around forms.

That's how my pet project: typed-form-actions was born.

This is a small library for React and Next.js that ties together FormData, zod, and useActionState so that forms remain native but are also typed and easy to work with.
What's the pain?

If you take Next.js App Router + Server Actions + zod, the typical scenario looks like this:

The user submits the form.

FormData arrives in the Server Action.

We extract the fields manually.

We assemble an object from the flat structure.

We validate it using zod.

We return errors for the fields.

On the client, we manually tie all this together with useActionState again.

So, the stack seems modern, but the amount of routine work is still very noticeable.

Here's a very typical code snippet without an additional layer of abstraction:

"use server";

import { z } from "zod";

const contactSchema = z.object({
name: z.string().min(2, "Name is too short."),
email: z.string().email("Enter a valid email."),
message: z.string().min(10, "Message is too short."),
});

export async function sendMessage(prevState: any, formData: FormData) {
const rawValues = {
name: formData.get("name"),
email: formData.get("email"),
message: formData.get("message"),
};

const parsed = contactSchema.safeParse(rawValues);

if (!parsed.success) {
return {
status: "error",
values: rawValues,
data: null,
formError: "Please correct the highlighted fields and try again.",
fieldErrors: parsed.error.flatten().fieldErrors,
submittedAt: Date.now(),
};
}

try {
await saveMessage(parsed.data);

return {
status: "success",
values: parsed.data,
data: { ok: true },
formError: null,
fieldErrors: {},
submittedAt: Date.now(),
};
} catch (error) {
return {
status: "error",
values: rawValues,
data: null,
formError: "Something went wrong.",
fieldErrors: {},
submittedAt: Date.now(),
};
}
}

This function itself isn't scary.

The problem is that it appears again, and again, and again. And if the form contains arrays, checkboxes, or nested fields like profile.name and links[0].href, the code becomes even longer.
What I wanted from the library

I didn't want to create another gigantic form framework.

On the contrary, I wanted a very narrow library with a clear purpose:

not break native HTML forms

not move everything into client-only logic

work well with Server Actions

work well with zod

обеспечить стабильное состояние действия

не заставляют вас вручную собирать одно и то же в каждом проекте

Итак, идея заключалась не в замене всех существующих решений для форм, а в решении очень специфической проблемы, связанной с современными формами React / Next.js. Каков конечный результат?

В конечном счете, API библиотеки построен на основе двух принципов:

createFormAction

useActionForm

1. createFormAction

Эта функция принимает схему Zod и обработчик и возвращает функцию, которую можно использовать в качестве действия для формы.

import { createFormAction } from "typed-form-actions";
import { z } from "zod";

const newsletterSchema = z.object({
email: z.string().email("Введите действительный адрес электронной почты."),
темы: z.array(z.string()).min(1, "Выберите хотя бы одну тему."),
маркетинг: z
.preprocess((value) => value === "on", z.boolean())
.default(false),
});

export const subscribeAction = createFormAction({
схема: newsletterSchema,
асинхронный обработчик(значений) {
возвращаться {
сообщение: Подписано ${values.email},
topicsCount: values.topics.length,
};
},
});
2. useActionForm

Это простая обертка над useActionState, предоставляющая удобный API для полей, ошибок и состояния ожидания.

"использовать клиент";

import { useActionForm } from "typed-form-actions/react";
import { subscribeAction } from "./actions";

export function NewsletterForm() {
const form = useActionForm(subscribeAction);

возвращаться (


Электронная почта <input {...form.getInputProps("email", { id: "email", type: "email" })} />
{form.getFieldError("email") ? (
<p id={form.getFieldErrorId("email")}>
{form.getFieldError("email")}
</p>
): нулевой}

<fieldset>
<legend>Темы</legend>
 
Top Bottom