Development··3 min read

React Server Actions: Pitfalls I Hit in Production

Server Actions looked simple. Then I actually used them.

It looked so clean at first

When I first saw Server Actions, my immediate thought was "so I don't need API routes anymore?" Server functions called directly from form submissions. The boilerplate reduction felt huge.

So I applied them to a new project right away. User registration, post CRUD, file uploads. Everything ran clean. For the first two weeks.

Error handling is trickier than expected

First pitfall. When a Server Action throws an error, how does the client find out? If you throw, it hits the error boundary on the client side -- but that's not the right pattern for form submission errors. "Incorrect password" should show as a message next to the form field, not trigger an error boundary.

Ended up with the pattern of returning error objects instead of throwing.

'use server'
 
export async function login(formData: FormData) {
  const email = formData.get('email') as string;
  const password = formData.get('password') as string;
 
  const user = await authenticate(email, password);
 
  if (!user) {
    return { error: 'Invalid email or password.' };
  }
 
  return { success: true };
}

I know this has become the standard pattern, but initially it felt weird. Treating errors as return values doesn't sit right instinctively.

The revalidatePath timing issue

Second pitfall. Calling revalidatePath invalidates the cache for that path, but the timing is subtle.

After creating a post, I called revalidatePath('/posts') then redirect('/posts'). But sometimes the new post didn't show up in the list. Refresh and it appeared. Cache invalidation seemed to process after the redirect.

Wasting half a day on this was painful. Eventually switched to revalidateTag and tagged the data fetching calls. But this process was barely documented, so it was trial and error the whole way.

File uploads hit a wall

Third pitfall. Implemented file uploads with Server Actions, and big files broke. The default request body size limit is 1MB. One image is fine. Multiple images or video files -- nope.

Had to increase serverActions.bodySizeLimit in next.config.ts, but finding that took a while. The error message just said "request entity too large" with no hint that it was a Server Actions config issue.

And showing upload progress is impossible with Server Actions. You need XMLHttpRequest or fetch streaming, which Server Actions don't support. So I ended up extracting file uploads into a separate API route. (Which kind of defeats the purpose of using Server Actions...)

The useActionState combo

Applied useActionState for form state management since it was supposed to be cleaner. Loading state management did get easier -- disabling buttons with isPending is straightforward.

But initial state setup is confusing. The return type needs to match the Server Action's return value, but initially there's nothing, so you put in an empty object, which doesn't match the type, so you end up with union types... and the TypeScript war begins.

So is it still worth using

It is. For simple form submissions (login, registration, comments), it's definitely more concise than API routes. But when complex requirements come in, you end up using API routes alongside them anyway.

By my count, Server Actions handle about 60% of all server communication needs. The remaining 40% still needs API routes.

"Server Actions replace API routes" is wrong. "Server Actions make simple server communication easier" is right. Getting that expectation calibrated matters a lot.

Related Posts