Handling Loading States and Validation with React Actions

In our previous post, we looked at how the new React Actions API lets you handle forms with simple, server-first functions — no onSubmit, no fetch. But what about user feedback?

Let’s look at how to handle:

  • ✅ Loading indicators
  • 🚫 Validation errors
  • 🎉 Success messages

⏳ Handling Loading States with useFormStatus

React 19 introduces useFormStatus, a new hook that lets you track form submission status directly inside your components — no need to manage your own loading state.

'use client';

import { useFormStatus } from 'react-dom';

function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Sending...' : 'Send'}
    </button>
  );
}

Use this button inside a form that uses a server action:

<form action={sendContact}>
  <input name="email" />
  <textarea name="message" />
  <SubmitButton />
</form>

⚠️ Displaying Validation Errors

You can throw errors from inside your action — and then catch them client-side with React’s new error handling patterns.

Let’s update our server action:

'use server';

export async function sendContact(formData: FormData) {
  const email = formData.get('email');

  if (!email || !email.toString().includes('@')) {
    throw new Error('Invalid email address');
  }

  // Continue processing...
}

And now show the error in your component using useFormState:

'use client';

import { useFormState } from 'react-dom';
import { sendContact } from '../actions';

function ContactForm() {
  const [state, formAction] = useFormState(sendContact, null);

  return (
    <form action={formAction}>
      <input name="email" />
      <textarea name="message" />
      {state && <p className="error">{state}</p>}
      <SubmitButton />
    </form>
  );
}

You just pass the sendContact function to useFormState — it handles calling the server and passing back errors or results.

Modify your action to return a string instead of throwing if you prefer:

export async function sendContact(_: FormData): Promise<string | null> {
  return 'Oops, something went wrong!';
}

🎉 Showing Success Messages

You can also return a success message or redirect after the form completes. Example:

export async function sendContact(formData: FormData) {
  // ... handle form

  return 'Thank you! Your message was sent.';
}

And then use it in your component like this:

const [message, formAction] = useFormState(sendContact, null);

return (
  <form action={formAction}>
    {/* form fields */}
    <SubmitButton />
    {message && <p className="success">{message}</p>}
  </form>
);

✅ Final Thoughts

With useFormState and useFormStatus, you can now build robust form UIs with built-in loading and error handling — all while keeping your business logic on the server.

No more local loading state, no more useEffect hacks, no more mixing server/client responsibilities. Just clean, declarative form logic.


Let me know if you’d like a deep dive on:

  • 🧪 Validating multiple fields with zod/yup
  • 🌐 Redirecting after form submission
  • 🔄 Resetting forms after success

Happy coding with React Actions!

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *