#Using Server Action

1 messages · Page 1 of 1 (latest)

warm patio
#

I’m building a survey service using Next.js 15 with App Router. One of the features is auto-saving form questions—similar to how Google Forms works.

Right now, I’m considering how to implement this auto-save functionality. Since it’s user-triggered and runs on the client side, I can’t directly use Prisma from the client. So I see two main options:

  1. API route handler – creating a traditional REST API endpoint (e.g. POST /api/questions/[id]) and calling it from the client.
  2. Server Actions – using a server function and calling it from a client component.

I’m leaning toward Server Actions, but I have concerns:

  • Since the input is being saved on every keystroke, I’m worried about race conditions, where a slower request might override a more recent one.
  • Debouncing helps reduce request count, but doesn’t completely prevent race conditions.
  • I’m wondering if there’s a recommended pattern or best practice to avoid race conditions when using Server Actions for auto-saving.

My questions:

  • Is it okay to use Server Actions for this kind of real-time update use case?
  • How should I manage race conditions in Server Actions, especially when updates are frequent and time-sensitive?
  • Would it be better to use API routes instead?
tribal kestrelBOT
#

Hi @warm patio. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

Using Server Actions for Auto-Save Functionality

Server Actions are indeed appropriate for your auto-save functionality in a Next.js 15 survey application. Let me address your concerns about race conditions and provide some recommendations.

Server Actions for Real-Time Updates

Yes, Server Actions are suitable for real-time update scenarios like auto-saving form questions. They're designed to handle data mutations and integrate well with Next.js's caching and revalidation architecture Server Actions and Mutations.

Managing Race Conditions

Your concern about race conditions with frequent updates is valid. Here are some approaches to handle this:

1. Sequential Execution

Server Actions are executed sequentially by design. As noted in a GitHub issue:

Server actions are blocking by default to make things like useOptimistic work
Github Issue: Server Actions: Blocking Behaviour

This means that if a user triggers multiple actions in quick succession, they'll be queued and processed in order, which can help prevent some race conditions.

2. Debouncing

As you mentioned, debouncing is recommended for cases where multiple events might be fired quickly:

'use client'

import { saveDraft } from './actions'
import { useCallback } from 'react'
import debounce from 'lodash/debounce' // or implement your own

export default function QuestionEditor() {
  // Debounce the save function
  const debouncedSave = useCallback(
    debounce(async (content) => {
      await saveDraft(content)
    }, 500),
    []
  )
#

return (
<form>
<textarea
name="content"
onChange={(e) => {
debouncedSave(e.target.value)
}}
/>
</form>
)
}


The Next.js docs specifically recommend debouncing for scenarios like yours:

> For cases like this, where multiple events might be fired in quick succession, we recommend **debouncing** to prevent unnecessary Server Action invocations.
[Server Actions and Mutations: Event Handlers](<https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#event-handlers>)

### 3. Versioning or Timestamps

To further protect against race conditions, you can implement a versioning system:

```javascript
'use server'

export async function saveDraft(content, timestamp) {
  // In your database query, only update if this is the most recent edit
  await prisma.question.updateMany({
    where: {
      id: questionId,
      lastEditedAt: { lt: new Date(timestamp) } // Only update if this edit is newer
    },
    data: {
      content,
      lastEditedAt: new Date(timestamp)
    }
  })
}

Server Actions vs. API Routes

For your use case, Server Actions offer several advantages over API routes:

  1. Less boilerplate: You don't need to create separate API endpoints
  2. React integration: You can use React features like useOptimistic for better UX
  3. Progressive enhancement: Forms work even if JavaScript is disabled

As mentioned in the docs:

You can progressively adopt Server Actions while still using client components. This allows you to remove boilerplate code to call an API route, and instead use React features like useActionState to handle loading and error states.
Using Server Actions in client components

#

Optimistic Updates for Better UX

To improve user experience, consider implementing optimistic updates:

'use client'

import { useOptimistic } from 'react'
import { saveQuestion } from './actions'

export function QuestionEditor({ initialQuestion }) {
  const [optimisticQuestion, updateOptimisticQuestion] = useOptimistic(
    initialQuestion,
    (state, newContent) => ({ ...state, content: newContent })
  )

  const handleChange = async (e) => {
    const newContent = e.target.value
    updateOptimisticQuestion(newContent)
    await saveQuestion(newContent)
  }

  return (
    <textarea 
      value={optimisticQuestion.content} 
      onChange={handleChange} 
    />
  )
}

This approach updates the UI immediately while the server action completes in the background Server Actions and Mutations: Optimistic Updates.

Conclusion

Server Actions are well-suited for your auto-save functionality. By combining debouncing with a versioning mechanism, you can effectively manage race conditions while benefiting from the simplicity and React integration that Server Actions provide.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: