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
andonSubmit
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 viaformData.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 thestate
. - ✅ Disable the form while uploading – Prevent users from double-submitting.
- ✅ Support multiple file uploads – Change
input type="file"
tomultiple
and loop through theFileList
. - ✅ 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.