skip to Main Content

I have the react client component, it’s a part of a next.js app, this component is a simple form that sends it’s formData to a next.js server action through an async function that will do the following steps:

  1. Set form button to be disabled & message to "Loading".
  2. Send formData to Next.JS via an async server action.
  3. Set form button to be enabled & message to error if error.

The problem seems to be that react is batching my setState calls, so in execution it’s as if step 1 doesn’t even happen, just step 2 + 3.

I can’t figure out how to circumvent this behaviour.
Here’s the source code:

"use client";

import { SendGiftAction } from "@/app/actions/SendGiftAction";
import { useState } from "react";

export default function GoldGiftForm()
{
    let [interactable, SetInteractable] = useState(true);
    let [message, setMessage] = useState("");

    async function Execute(data: FormData)
    {
        //These won't change the UI
        SetInteractable(false);
        setMessage("Loading");
        //-------------------------

        //Next.JS Server action
        let response = await SendGiftAction(data);

        //These will change the UI
        SetInteractable(true);
        setMessage(response.Success ? "" : response.Error);
        //------------------------
    }

    return (
        <form action={Execute}>
            <h3>Send Gold Gift</h3>

            <input
                name="userID"
                type="text"
                placeholder="User ID"
            />

            <input
                name="gold"
                type="number"
                placeholder="Gold"
            />

            <button type="submit" disabled={!interactable}> Send Gift </button>

            <p>{message}</p>
        </form>
    );
}

This is a code snippet for SendGiftAction: https://pastebin.com/YYQeG0m4

Which is a wrapper that validates data and invokes SendSystemGift:
https://pastebin.com/uD0LLEFL

Both are standard async functions really.

2

Answers


  1. You need to understand how async call will excute in your function

    //These will run immediately
        SetInteractable(false);
        setMessage("Loading");
        //-------------------------
    
        //This will take time to excute 
        let response = await SendGiftAction(data);
    
        //So in this time it will excute before api call internall
        SetInteractable(true);
        setMessage(response.Success ? "" : response.Error);
    

    This is modified code

    //These will run immediately
        SetInteractable(false);
        setMessage("Loading");
    
        //This will take time to excute 
        let response = await SendGiftAction(data).then((response)=>{SetInteractable(true);
        setMessage(response.Success ? "" : response.Error);});
    
    Login or Signup to reply.
  2. The issue you’re encountering arises from the fact that React batches state updates inside asynchronous functions like await or promises. So, when you call SetInteractable(false) and setMessage("Loading"), React delays rendering until the await SendGiftAction(data) finishes, which makes it seem like those UI updates don’t happen before the async call completes.

    To address this, you can force React to apply the first set of state changes before executing the await by flushing the state updates. One way to do this is to use flushSync from react-dom, which ensures that React immediately processes the state updates before continuing with the async logic.

    Solution: Use flushSync to force immediate state updates
    Here’s how you can modify your Execute function:

    "use client";
    
    import { SendGiftAction } from "@/app/actions/SendGiftAction";
    import { useState } from "react";
    import { flushSync } from "react-dom";
    
    export default function GoldGiftForm() {
        let [interactable, SetInteractable] = useState(true);
        let [message, setMessage] = useState("");
    
        async function Execute(data: FormData) {
            // Force React to update the state immediately
            flushSync(() => {
                SetInteractable(false);
                setMessage("Loading");
            });
    
            // Next.JS Server action
            let response = await SendGiftAction(data);
    
            // Update UI after server action completes
            SetInteractable(true);
            setMessage(response.Success ? "" : response.Error);
        }
    
        return (
            <form action={Execute}>
                <h3>Send Gold Gift</h3>
    
                <input
                    name="userID"
                    type="text"
                    placeholder="User ID"
                />
    
                <input
                    name="gold"
                    type="number"
                    placeholder="Gold"
                />
    
                <button type="submit" disabled={!interactable}> Send Gift </button>
    
                <p>{message}</p>
            </form>
        );
    }
    

    Explanation:

    • flushSync ensures that React processes and renders the state updates
      (SetInteractable(false) and setMessage("Loading")) before proceeding
      with the async operation.
    • This approach forces the UI to update immediately, so the user will
      see the "Loading" message and the button will be disabled before the
      async function starts.
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search