useActionState ile Alan Düzeyinde Doğrulama Hataları
İşlemlerden yapılandırılmış doğrulama sonuçları döndürün ve her alanın hata iletilerini ilgili yerde görüntüleyin.
useActionState ile Alan Düzeyinde Doğrulama Hataları, CoddyKit'te ücretsiz bir Next.js 15 Fullstack (App Router + Server Actions) dersidir. Bu, 4 dersinin 3. dersidir. Aşağıdan dersin tamamını ücretsiz okuyabilir, sonra tarayıcıda yerleşik kod editörü ve 7/24 yapay zeka koçu ile uygulamalı olarak pratik yapabilirsin. Bu, Next.js 15 Fullstack (App Router + Server Actions) öğrenme yolunun bir parçasıdır ve ilerlemeniz web ve CoddyKit uygulaması arasında senkronize olur. Next.js 15 Fullstack (App Router + Server Actions) kursu toplamda 4 dersten oluşur.
Bu dersin bazı bölümleri henüz çevrilmemiş olup İngilizce olarak gösterilmektedir.
Why Field-Level Errors?
When a form fails validation, users need to know exactly which field is wrong and why. A single banner saying "Something went wrong" forces them to guess.
In Next.js 15, Server Actions paired with useActionState let you return a structured result from the server and render an inline error right beneath each input.
emailis invalid → show the error under the email fieldpasswordis too short → show it under the password field
This lesson shows how to return that structure and render it cleanly.
Shaping the Action State
Start by deciding the shape of what your action returns. A good pattern keeps per-field errors in an errors map keyed by field name, where each value is an array of messages.
Defining a TypeScript type makes the contract explicit for both the action and the component.
export type FieldErrors = {
email?: string[];
password?: string[];
};
export type SignupState = {
errors?: FieldErrors;
// value the user typed, so we can re-fill the form
values?: { email?: string };
// a top-level message for non-field errors
message?: string;
};
export const initialState: SignupState = {};Validating with Zod in the Action
A schema library like Zod gives you both validation and a ready-made error structure. Call safeParse so failures don't throw — instead you inspect the result.
flatten().fieldErrors returns exactly the { field: string[] } shape we designed, so it maps directly onto our state.
import { z } from 'zod';
const SignupSchema = z.object({
email: z.string().email('Enter a valid email'),
password: z.string().min(8, 'At least 8 characters'),
});
const result = SignupSchema.safeParse({
email: 'not-an-email',
password: '123',
});
if (!result.success) {
// { email: ['Enter a valid email'], password: ['At least 8 characters'] }
console.log(result.error.flatten().fieldErrors);
}The Server Action Signature
An action used with useActionState receives two arguments: the previous state and the submitted FormData. It must return the next state.
Mark the file or function with 'use server'. Read values with formData.get(...), validate, and return errors instead of throwing.
'use server';
import { z } from 'zod';
import type { SignupState } from './state';
const SignupSchema = z.object({
email: z.string().email('Enter a valid email'),
password: z.string().min(8, 'At least 8 characters'),
});
export async function signup(
prevState: SignupState,
formData: FormData,
): Promise<SignupState> {
const email = String(formData.get('email') ?? '');
const password = String(formData.get('password') ?? '');
const parsed = SignupSchema.safeParse({ email, password });
if (!parsed.success) {
return {
errors: parsed.error.flatten().fieldErrors,
values: { email }, // keep email, never echo the password
};
}
// ...persist the user here...
return { message: 'Account created' };
}Wiring useActionState
In your client component, call useActionState(action, initialState). It returns a tuple:
state— the latest value your action returnedformAction— pass this to the form'sactionpropisPending— true while the action runs (great for disabling the button)
Remember the 'use client' directive — hooks only run in client components.
'use client';
import { useActionState } from 'react';
import { signup } from './actions';
import { initialState } from './state';
export function SignupForm() {
const [state, formAction, isPending] = useActionState(
signup,
initialState,
);
return (
<form action={formAction}>
{/* inputs go here */}
<button disabled={isPending}>
{isPending ? 'Creating...' : 'Sign up'}
</button>
</form>
);
}Rendering an Inline Error
To show a field error, read it from state.errors?.fieldName. Because each field holds a string array, render the first message (or map over all of them).
Use optional chaining so the very first render — when errors is undefined — doesn't crash.
<div>
<label htmlFor="email">Email</label>
<input
id="email"
name="email"
type="email"
defaultValue={state.values?.email}
/>
{state.errors?.email && (
<p className="error">{state.errors.email[0]}</p>
)}
</div>Accessibility: Linking Errors to Inputs
Screen readers should announce the error and tie it to the field. Two attributes do the heavy lifting:
aria-invalid— settruewhen the field has an erroraria-describedby— point to the id of the error element
Give the error element a stable id and a polite live region so updates are read aloud.
<input
id="email"
name="email"
type="email"
defaultValue={state.values?.email}
aria-invalid={!!state.errors?.email}
aria-describedby="email-error"
/>
{state.errors?.email && (
<p id="email-error" className="error" aria-live="polite">
{state.errors.email[0]}
</p>
)}Preserving User Input
Because the form re-renders from server state, an uncontrolled input would lose what the user typed. Return the safe values from the action and feed them back with defaultValue.
Never echo passwords back to the client. Only return non-sensitive fields like email so the user doesn't retype everything after a validation error.
// in the action, on failure:
return {
errors: parsed.error.flatten().fieldErrors,
values: { email }, // safe to round-trip
// password is intentionally omitted
};
// in the component:
<input name="email" defaultValue={state.values?.email} />
<input name="password" type="password" /> {/* always blank */}A Pure Validation Helper You Can Test
Validation logic doesn't need Next.js to be correct. Extract it into a pure function that takes plain values and returns the same { field: string[] } error map. This is trivially unit-testable and runnable on any judge.
type FieldErrors = { email?: string[]; password?: string[] };
function validateSignup(email: string, password: string): FieldErrors {
const errors: FieldErrors = {};
const emailOk = /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email);
if (!emailOk) errors.email = ['Enter a valid email'];
if (password.length < 8) errors.password = ['At least 8 characters'];
return errors;
}
const e1 = validateSignup('bad', '123');
console.log(e1); // { email: [...], password: [...] }
const e2 = validateSignup('a@b.co', 'longenough');
console.log(Object.keys(e2).length === 0); // true
console.log(JSON.stringify(validateSignup('x@y.com', 'short')));Multiple Errors per Field
A single field can fail several rules at once (empty, too short, wrong format). Since each entry is an array, you can render every message as a list.
Zod naturally accumulates multiple issues per field, so fieldErrors.password may contain more than one string.
{state.errors?.password && (
<ul className="error-list">
{state.errors.password.map((msg) => (
<li key={msg}>{msg}</li>
))}
</ul>
)}Top-Level vs Field Errors
Not every failure belongs to a field. A duplicate-email conflict from the database, or a generic server fault, is a form-level message.
Keep both channels in your state: errors for per-field issues and message for the whole form. Render the top-level message above the fields so it isn't missed.
// in the action, after a successful parse:
try {
await createUser(email, password);
} catch (err) {
if (isUniqueViolation(err)) {
return { errors: { email: ['Email already in use'] } };
}
return { message: 'Something went wrong. Please try again.' };
}
// in the component, above the fields:
{state.message && <p role="alert">{state.message}</p>}Quick Check
Test your understanding of the action contract used by useActionState.
Recap
You can now build field-level validation with Server Actions and useActionState:
- Shape the state with an
errorsmap of{ field: string[] }, plus optionalvaluesand a top-levelmessage. - Validate in the action with
safeParseand returnflatten().fieldErrors— return, never throw. - Wire the hook with
useActionState(action, initialState)and passformActionto the form; useisPendingfor the button. - Render inline from
state.errors?.field, witharia-invalidandaria-describedbyfor accessibility. - Preserve input via
defaultValue, but never echo passwords. - Separate channels: per-field
errorsvs form-widemessage.
Sıkça Sorulan Sorular
“useActionState ile Alan Düzeyinde Doğrulama Hataları” dersi ücretsiz mi?
Evet — “useActionState ile Alan Düzeyinde Doğrulama Hataları” dersin tüm metni burada web'de ücretsiz olarak okunabilir. Etkileşimli olarak pratik yapmak (yerleşik kod editörü ve 7/24 yapay zeka koçu) ve Next.js 15 Fullstack (App Router + Server Actions) kursunun geri kalanını açmak için CoddyKit PRO'ya yükselt. Next.js 15 Fullstack (App Router + Server Actions) kursu toplamda 4 dersten oluşur.
“useActionState ile Alan Düzeyinde Doğrulama Hataları” dersinde ne öğreneceğim?
İşlemlerden yapılandırılmış doğrulama sonuçları döndürün ve her alanın hata iletilerini ilgili yerde görüntüleyin. Next.js 15 Fullstack (App Router + Server Actions) ile uygulamalı kodu tarayıcıda doğrudan çalıştırarak pratik yaparsın ve 7/24 yapay zeka koçu dersi çalışırken sorularını yanıtlar.
Next.js 15 Fullstack (App Router + Server Actions) öğrenmeye başlamak için deneyim gerekli mi?
Önceden deneyim gerekmez. CoddyKit'te Next.js 15 Fullstack (App Router + Server Actions), başlangıçtan ileri seviyeye kadar yapılandırıldığı için buradan başlayabilir veya başından başlayıp kendi hızında ilerleme yapabilirsin. Bu, 4 dersinin 3. dersidir.
“useActionState ile Alan Düzeyinde Doğrulama Hataları” dersi ne kadar sürer?
Çoğu CoddyKit dersi yaklaşık 5–10 dakika sürer. Her biri kısa ve etkileşimli olduğu için sabit ilerleme yaparsın ve web ile uygulama arasında tam olarak bıraktığın yerden devam edebilirsin.
Bu Next.js 15 Fullstack (App Router + Server Actions) dersinde kod yazıp çalıştırabilir miyim?
Evet. Her Next.js 15 Fullstack (App Router + Server Actions) dersi yerleşik bir kod editörü içerir, bu sayede tarayıcıda gerçek kod yazıp çalıştırabilir ve anlık yapay zeka geri bildirimi alırsın — yerel kurulum gerekli değildir.
Bu kursun tüm dersleri
- Form Action Prop'u ile Aşamalı Geliştirme
- useFormStatus ile Bekleme ve Yükleme Durumları
- useActionState ile Alan Düzeyinde Doğrulama Hataları
- useOptimistic ile Anında Geri Bildirim