File Upload with useActionState in React

Uploading files in React is a common but often complex task. It involves handling form submissions, extracting file data, validating inputs, and optionally sending files to a backend server or cloud storage. With the release of React 18.2, the introduction of the useActionState hook has made form handling more declarative, structured, and cleaner—especially when combined with modern React frameworks like Next.js.

In this blog, we’ll explore how to implement file uploads using useActionState. You’ll learn how the hook works, how to structure your upload logic, how to handle validation and feedback, and how to integrate it with server actions in frameworks like Next.js App Router.


What is useActionState?

useActionState is a built-in React hook introduced in version 18.2. It simplifies handling user actions—most commonly form submissions—by centralizing the submission logic in a single asynchronous function.

Here’s how it works:

  • You define a reducer-style function that receives previous state and FormData
  • You return a new state (usually an object containing success, error, or status info)
  • React re-renders the component with the updated state

The hook returns:

  • The latest action state
  • An action function you assign to a form’s action attribute

Basic Syntax

const [state, actionFn] = useActionState(async (prevState, formData) => {
  // Your logic
  return newState;
}, initialState);

Why Use useActionState for File Uploads?

Traditionally, file uploads require you to:

  • Handle onChange and onSubmit manually
  • Use useState to track selected files and upload status
  • Write boilerplate fetch or axios calls
  • Manage loading and error states

With useActionState, you can skip all that. It handles form submission natively, gives you direct access to the file via FormData, and centralizes error and success handling in a declarative way.


File Upload with useActionState: Step-by-Step Guide

Let’s walk through how to implement a file upload form using useActionState.

1. Setup React Environment

Ensure you are using React 18.2+. If you are using Next.js, make sure your component is a Client Component (starts with 'use client').

2. Create the File Upload Component

'use client';

import { useActionState } from 'react';

export default function FileUploadForm() {
  const [uploadState, formAction] = useActionState(async (prevState, formData) => {
    const file = formData.get('file');

    if (!file || !(file instanceof File)) {
      return { error: 'Please select a file to upload.', success: null };
    }

    // Simulate uploading to a server
    await new Promise(resolve => setTimeout(resolve, 1000));

    return {
      success: `File "${file.name}" uploaded successfully!`,
      error: null,
    };
  }, { success: null, error: null });

  return (
    <form action={formAction} encType="multipart/form-data">
      <input type="file" name="file" accept="image/*,.pdf" />
      <button type="submit">Upload</button>

      {uploadState.error && <p style={{ color: 'red' }}>{uploadState.error}</p>}
      {uploadState.success && <p style={{ color: 'green' }}>{uploadState.success}</p>}
    </form>
  );
}

Explanation:

  • The form uses encType="multipart/form-data" for file uploads.
  • The file input is named file, and accessed via formData.get('file').
  • The logic checks for presence and type of file before simulating an upload.

Validating File Size and Type

Let’s add validation for file size and file type inside the reducer function.

const MAX_SIZE = 5 * 1024 * 1024; // 5 MB

const [uploadState, formAction] = useActionState(async (prevState, formData) => {
  const file = formData.get('file');

  if (!file || !(file instanceof File)) {
    return { error: 'File is required.', success: null };
  }

  if (!['image/png', 'image/jpeg', 'application/pdf'].includes(file.type)) {
    return { error: 'Only PNG, JPEG, and PDF files are allowed.', success: null };
  }

  if (file.size > MAX_SIZE) {
    return { error: 'File must be under 5MB.', success: null };
  }

  await new Promise(res => setTimeout(res, 1000));

  return { success: `Uploaded "${file.name}" (${file.size} bytes)`, error: null };
}, { success: null, error: null });

This pattern allows you to do full client-side validation in a single block of logic while keeping your UI reactive to the result.


Handling File Upload to a Server

If you want to upload the file to an actual backend (or server action in Next.js), you can replace the simulated delay with a fetch call or stream the file to a cloud storage service.

await fetch('/api/upload', {
  method: 'POST',
  body: formData, // includes file and any other form fields
});

If you’re using Next.js App Router, you can also handle this using a server action and call it from the client using useActionState.


Using Server Actions with useActionState for File Uploads

In Next.js, you can define a server action and use it with useActionState.

Create a server action

// app/actions/uploadFile.ts
'use server';

import { writeFile } from 'fs/promises';
import path from 'path';

export async function uploadFile(prevState: any, formData: FormData) {
  const file = formData.get('file');

  if (!file || !(file instanceof File)) {
    return { error: 'No file uploaded', success: null };
  }

  const bytes = await file.arrayBuffer();
  const buffer = Buffer.from(bytes);
  const filePath = path.join(process.cwd(), 'uploads', file.name);

  await writeFile(filePath, buffer);

  return { success: `File "${file.name}" uploaded successfully`, error: null };
}

Use it in your client component

'use client';

import { useActionState } from 'react';
import { uploadFile } from '../actions/uploadFile';

export default function UploadForm() {
  const [uploadState, formAction] = useActionState(uploadFile, {
    success: null,
    error: null,
  });

  return (
    <form action={formAction} encType="multipart/form-data">
      <input type="file" name="file" />
      <button type="submit">Upload</button>

      {uploadState.error && <p style={{ color: 'red' }}>{uploadState.error}</p>}
      {uploadState.success && <p style={{ color: 'green' }}>{uploadState.success}</p>}
    </form>
  );
}

This approach leverages useActionState with server logic, and the file upload is handled securely on the backend.


UX Tips for File Upload Forms

  • Show loading state – Use a loading flag inside the reducer or derive it from the state.
  • Disable the form while uploading – Prevent users from double-submitting.
  • Support multiple file uploads – Change input type="file" to multiple and loop through the FileList.
  • Use previews – Show a thumbnail for image uploads.
  • Auto-reset form after success – You can use a key prop to force remounting the form.

Conclusion

Using useActionState for file uploads in React gives you a much cleaner, centralized, and declarative way to manage form state and submission. Whether you’re uploading files locally, sending them to a server, or validating them before submission, useActionState lets you keep your logic in one place and your UI in sync with real-time feedback.

By leveraging this hook along with modern tools like Next.js App Router and server actions, you can simplify the entire upload flow—from input to backend—in just a few lines of code.

If you’re building a dashboard, admin panel, CMS, or any app that requires file handling, give useActionState a try. You’ll find it’s easier to use, easier to scale, and easier to reason about than traditional approaches.

Leave a Comment

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

Scroll to Top