What is the useFormState hook?

👨‍💻 Frontend Developer 🟠 May come up 🎚️ Medium
#React #Hooks

Brief Answer

useFormState is a React hook for working with forms and server actions:

  1. Form state management — tracks data and status 📝
  2. Server actions — integration with Server Actions 🖥️
  3. Automatic handling — loading, error, success states ⚡
  4. Progressive enhancement — works without JavaScript 🌐
  5. Optimistic updates — instant feedback ✨
  6. Validation — built-in error support ❌
import { useFormState } from 'react-dom';
 
function ContactForm() {
  const [state, formAction] = useFormState(submitForm, null);
  
  return (
    <form action={formAction}>
      <input name="email" type="email" />
      <button type="submit">Submit</button>
      {state?.error && <p>{state.error}</p>}
    </form>
  );
}

Full Answer

useFormState is a modern hook for working with forms in React! It simplifies integration with server actions and form state management. 🚀

Basic Syntax

import { useFormState } from 'react-dom';
 
const [state, formAction] = useFormState(action, initialState);

Hook Parameters

// action - server function
async function submitForm(prevState, formData) {
  // Process form data
  const email = formData.get('email');
  
  try {
    await sendEmail(email);
    return { success: true, message: 'Sent!' };
  } catch (error) {
    return { error: 'Send error' };
  }
}
 
// initialState - initial state
const initialState = { message: null, error: null };
 
function MyForm() {
  const [state, formAction] = useFormState(submitForm, initialState);
  
  return (
    <form action={formAction}>
      <input name="email" required />
      <button type="submit">Submit</button>
      
      {state?.success && <p>✅ {state.message}</p>}
      {state?.error && <p>❌ {state.error}</p>}
    </form>
  );
}

Practical Examples

1. Contact Form

async function contactAction(prevState, formData) {
  const name = formData.get('name');
  const email = formData.get('email');
  const message = formData.get('message');
  
  // Validation
  if (!name || !email || !message) {
    return { error: 'Fill all fields' };
  }
  
  try {
    await sendContactForm({ name, email, message });
    return { success: true, message: 'Message sent!' };
  } catch (error) {
    return { error: 'Server error' };
  }
}
 
function ContactForm() {
  const [state, formAction] = useFormState(contactAction, null);
  
  return (
    <form action={formAction}>
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <textarea name="message" placeholder="Message" required />
      
      <button type="submit">Submit</button>
      
      {state?.success && (
        <div className="success">✅ {state.message}</div>
      )}
      {state?.error && (
        <div className="error">❌ {state.error}</div>
      )}
    </form>
  );
}

2. Registration Form

async function registerAction(prevState, formData) {
  const username = formData.get('username');
  const password = formData.get('password');
  
  // Validation
  if (password.length < 6) {
    return { error: 'Password must be at least 6 characters' };
  }
  
  try {
    const user = await createUser({ username, password });
    return { success: true, user };
  } catch (error) {
    return { error: 'User already exists' };
  }
}
 
function RegisterForm() {
  const [state, formAction] = useFormState(registerAction, null);
  
  return (
    <form action={formAction}>
      <input name="username" placeholder="Username" required />
      <input name="password" type="password" placeholder="Password" required />
      
      <button type="submit">Register</button>
      
      {state?.success && (
        <p>Welcome, {state.user.username}!</p>
      )}
      {state?.error && <p className="error">{state.error}</p>}
    </form>
  );
}

3. File Upload Form

async function uploadAction(prevState, formData) {
  const file = formData.get('file');
  
  if (!file || file.size === 0) {
    return { error: 'Select a file' };
  }
  
  if (file.size > 5 * 1024 * 1024) {
    return { error: 'File too large (max 5MB)' };
  }
  
  try {
    const url = await uploadFile(file);
    return { success: true, url };
  } catch (error) {
    return { error: 'Upload failed' };
  }
}
 
function UploadForm() {
  const [state, formAction] = useFormState(uploadAction, null);
  
  return (
    <form action={formAction}>
      <input name="file" type="file" accept="image/*" required />
      <button type="submit">Upload</button>
      
      {state?.success && (
        <div>
          <p>✅ File uploaded!</p>
          <img src={state.url} alt="Uploaded" width="200" />
        </div>
      )}
      {state?.error && <p className="error">{state.error}</p>}
    </form>
  );
}

Integration with useFormStatus

import { useFormState } from 'react-dom';
import { useFormStatus } from 'react-dom';
 
function SubmitButton() {
  const { pending } = useFormStatus();
  
  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Submitting...' : 'Submit'}
    </button>
  );
}
 
function MyForm() {
  const [state, formAction] = useFormState(submitAction, null);
  
  return (
    <form action={formAction}>
      <input name="data" required />
      <SubmitButton />
      
      {state?.error && <p>{state.error}</p>}
    </form>
  );
}

Working with Validation

async function validateAction(prevState, formData) {
  const email = formData.get('email');
  const age = formData.get('age');
  
  const errors = {};
  
  // Email validation
  if (!email.includes('@')) {
    errors.email = 'Invalid email format';
  }
  
  // Age validation
  if (age < 18) {
    errors.age = 'Age must be 18+';
  }
  
  if (Object.keys(errors).length > 0) {
    return { errors };
  }
  
  // Save data
  await saveUser({ email, age });
  return { success: true };
}
 
function ValidationForm() {
  const [state, formAction] = useFormState(validateAction, null);
  
  return (
    <form action={formAction}>
      <div>
        <input name="email" type="email" placeholder="Email" />
        {state?.errors?.email && (
          <span className="error">{state.errors.email}</span>
        )}
      </div>
      
      <div>
        <input name="age" type="number" placeholder="Age" />
        {state?.errors?.age && (
          <span className="error">{state.errors.age}</span>
        )}
      </div>
      
      <button type="submit">Save</button>
      
      {state?.success && <p>✅ Data saved!</p>}
    </form>
  );
}

Optimistic Updates

import { useOptimistic } from 'react';
 
async function addCommentAction(prevState, formData) {
  const comment = formData.get('comment');
  
  // Simulate server delay
  await new Promise(resolve => setTimeout(resolve, 1000));
  
  const newComment = {
    id: Date.now(),
    text: comment,
    author: 'User'
  };
  
  return { success: true, comment: newComment };
}
 
function CommentForm({ comments, onAddComment }) {
  const [state, formAction] = useFormState(addCommentAction, null);
  const [optimisticComments, addOptimisticComment] = useOptimistic(
    comments,
    (state, newComment) => [...state, newComment]
  );
  
  return (
    <div>
      <form 
        action={async (formData) => {
          const comment = formData.get('comment');
          addOptimisticComment({
            id: Date.now(),
            text: comment,
            author: 'User'
          });
          
          const result = await formAction(formData);
          if (result?.success) {
            onAddComment(result.comment);
          }
        }}
      >
        <textarea name="comment" placeholder="Your comment" />
        <button type="submit">Add</button>
      </form>
      
      <div>
        {optimisticComments.map(comment => (
          <div key={comment.id}>{comment.text}</div>
        ))}
      </div>
    </div>
  );
}

useFormState Benefits

  1. Progressive enhancement — works without JS 🌐
  2. Easy to use — minimal code ✨
  3. Automatic handling — loading/error states 🔄
  4. SEO-friendly — server rendering 🔍
  5. Type safety — with TypeScript 🛡️

Best Practices

  1. Validate data on server and client ✅
  2. Handle errors properly ❌
  3. Show loading state to user ⏳
  4. Use optimistic updates for UX ⚡
// ✅ Correct
async function goodAction(prevState, formData) {
  try {
    // Validation
    const data = validateFormData(formData);
    
    // Processing
    const result = await processData(data);
    
    return { success: true, data: result };
  } catch (error) {
    return { error: error.message };
  }
}

Common Mistakes

Wrong:

// Forgot error handling
async function badAction(prevState, formData) {
  const result = await api.call(formData); // Can crash
  return result;
}

Correct:

// Proper error handling
async function goodAction(prevState, formData) {
  try {
    const result = await api.call(formData);
    return { success: true, data: result };
  } catch (error) {
    return { error: 'Something went wrong' };
  }
}

Conclusion

useFormState is a powerful tool for working with forms:

  • Simplifies server action integration
  • Automatically manages form state
  • Supports progressive enhancement
  • Improves user experience

Use it to create modern and responsive forms! 🎯