React

Verified against Claude Code · 2026-07-18

Convert a form's manual state juggling to React 19 Actions

Migrates an onSubmit-plus-useState form to useActionState, useFormStatus, and useOptimistic where it genuinely helps, instead of hand-rolled pending and error booleans.

Claude CodeCursorGitHub Copilot ChatChatGPT

The prompt

Ready to copy — highlighted parts are example details you can swap.

<role>
You are migrating a React form from manual onSubmit + useState plumbing to React 19's Actions APIs — the form action prop, useActionState, useFormStatus, and useOptimistic where it genuinely helps.
</role>

<current_form>
A NewsletterSignup form with useState for email, isSubmitting, and error, an onSubmit handler that calls preventDefault, sets isSubmitting, awaits a fetch, and sets error or success.
</current_form>

<validation_rules>
Email must be non-empty and match a basic email pattern.
</validation_rules>

<submit_behavior>
On success, clear the field and show a confirmation message inline; on failure, show the server error message next to the field without clearing it.
</submit_behavior>

<migration_rules>
- Replace the manual isSubmitting/error/success useState trio with useActionState wrapping a single async action function that does the validation and the submission.
- The action function returns a typed result object (e.g. { error: string | null } or a field-level error map) — never throw past the action boundary for expected validation failures, only for genuinely unexpected errors.
- Use useFormStatus inside a child submit-button component, not the form component itself, to disable the button and show pending state — useFormStatus only reads status from the nearest parent form when called from a descendant of it, and returns default (non-pending) values if called in the same component that renders the form.
- Only add useOptimistic if the UI needs to show the result before the server confirms it (a message appearing instantly in a list). Don't add it just because it's available — most forms should wait for the real result.
- Keep client-side validation for instant per-field feedback, but the action's own validation is the source of truth — don't let the two disagree.
</migration_rules>

<output_format>
1. The migrated form component and its submit-button child component, in separate code blocks.
2. The action function, with its return type made explicit.
3. A short note on which useState calls were removed and what replaced each one.
</output_format>
Customize the highlighted detailsoptional — the prompt above already works

Why this works

The child-component rule for useFormStatus is load-bearing, not stylistic: React's own documentation is explicit that useFormStatus must be called from a component rendered inside the form, and calling it in the same component that renders the <form> tag itself returns the default, always-false pending state — a bug that looks like it works in a quick test and then silently never shows a pending state in production. Collapsing three separate useState calls into one useActionState call removes an entire class of bug where isSubmitting, error, and the actual in-flight request can drift out of sync (a fetch that finishes after a component update, for instance) because the pending state now comes directly from the transition React is already tracking, not from a boolean the developer has to remember to flip back. Restricting useOptimistic to cases where the UI genuinely shows a result before confirmation stops the common mistake of adding it to a form where nothing is actually rendered optimistically, which just adds indirection with no visible benefit.

What you get back

async function subscribeAction(prevState, formData) { const email = formData.get('email'); if (!email || !/^[^@]+@[^@]+\.[^@]+$/.test(String(email))) { return { error: 'Enter a valid email address.' }; } const res = await fetch('/api/subscribe', { method: 'POST', body: formData }); if (!res.ok) return { error: 'Something went wrong — try again.' }; return { error: null }; } // SubmitButton.tsx function SubmitButton() { const { pending } = useFormStatus(); return <button disabled={pending}>{pending ? 'Subscribing…' : 'Subscribe'}</button>; } Removed: isSubmitting → replaced by useFormStatus's pending. error → replaced by useActionState's returned state. success → replaced by state.error === null after a submission has occurred.

Verified against

Claude Code Sonnet 4.6 · 2026-07-18

Cursor 2.1 · 2026-07-27

Changelog

  • 2026-07-18 Initial publish, verified against Claude Code (Sonnet 4.6) and Cursor 2.1 on React 19.

Need this built into your business?

If a prompt isn't enough — custom software, built and maintained for you — that's Scult's day job.

EXPLORE CUSTOM SOFTWARE
All React prompts

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY