Next.js Discord

Discord Forum

Using useOptimistic to instantly update quantities in a cart. How debounce the server action?

Unanswered
Transvaal lion posted this in #help-forum
Open in Discord
Transvaal lionOP
<form
  action={async () => {
    // use useOptimistic to increment quantity state in parent
    updateQuantity(1)
    
    // server action that updates the cart with the quantity
    // if I quickly click on the increase quantity button this
    // function will be fired many times quickly. I don't want
    // the below server action to run more than eg. 300 ms after
    // THE LAST click. So how do I debounce this server action call?
    await action();
  }}
>

21 Replies

@Transvaal lion <form action={async () => { // use useOptimistic to increment quantity state in parent updateQuantity(1) // server action that updates the cart with the quantity // if I quickly click on the increase quantity button this // function will be fired many times quickly. I don't want // the below server action to run more than eg. 300 ms after // THE LAST click. So how do I debounce this server action call? await action(); }} >
You can create a basic quantity counter. It a useState that functions as a counter. So it's start at 0 and if you click the button, it will be counted 1 up. For the debounce effect you can use this example component: https://paste.gg/p/B33fb0n3/4f3e2f02b0c441f58d95931712dcdd6d

Then you can use the debounce to execute your function. You can do it like this:
    const debouncedSearch = useDebounce(quantityCounter, 300); // time in ms

    useEffect(() => {
        execute({cartItemId: "someid"}) // example execution
    }, [debouncedSearch]);
Transvaal lionOP
The problem is that so far I haven't found a way to debounce a server action
Do it on the client
Transvaal lionOP
debouncing a normal function / event handler is easy enough, but the server action I can't figure out how to debounce
But I get a lot of server action requests and ideally I only want to hit it once
so if the user clicks 10 times in 0.2 seconds, you only want to execute the server actions ones and add 10 times the same item to the cart, right?
Transvaal lionOP
yes. Optimistically update for every click, but debounce the server action so it will only get called (and update the cart on the server) after the set delay after the 10th click
@Transvaal lion yes. Optimistically update for every click, but debounce the server action so it will only get called (and update the cart on the server) after the set delay after the 10th click
yea, than you should do this:
    const debouncedValue = useDebounce(quantityCounter, 300); // time in ms

    useEffect(() => {
        execute({cartItemId: "someid", quantity: quantityCounter}) // example execution
    }, [debouncedValue]);

Like this you can directly at the amount once if the last click is 300ms in the past. Of course you can also set the 300ms to 100ms or 1000ms. You can chose how often you want to call the server actions. With this code example, [this behavior](https://nextjs-forum.com/post/1240465429542207498#message-1240536264625750048) will work
Transvaal lionOP
Thanks for your suggestion. If it works that is great! Just so I understand:
"execute" is the server action right? Right now I'm using a "form" tag with the server action in the "action" attribute and a button of type "submit". So you're suggesting to bypass the form, add an onClick handler on the button of type "button" and only update the quantity counter via useState. Then have another debouncedValue that updates every other 300ms and once that happens you fire the server action from useEffect... wouldn't it be possible to just use an onClick handler where you set the optimistic update, and then wrap the server action in some kind of debounce function?
I kinda wanna use useOptimistic if possible since it does roll back if the server action doesnøt succeed
yea sorry, my bad. execute is from next-safe-action that I use for all my server actions stuff. You might also want to use it, because they have a useOptimisticAction [here](https://next-safe-action.dev/docs/usage-from-client/hooks/useoptimisticaction) that executes the server action in the background and directly updates the value. But there is no debounce in it.

Yes, you clarified it correctly. The onSubmit function of the form just counts the quantity up (you can directly display it (optimistic update)) and because the up count the useDebounce will be triggert and if the useDebounce won't receive another click in the next 300ms (in this case) it will updates it's value. Because it updates it's value the useEffect will be triggert, that then triggers the server action. So it's a debounced server action ^^
If you need to rollback if something goes wrong, add a .catch to the server action and set the quantityCount to 0 to remove the optimistic update back to default.
Transvaal lionOP
Thanks a lot - I'm a bit hesitant of pulling in libraries if I can achieve it with Next.js's native server actions without adding too much complexity, but at least you've shown me a way to get it working. I might just have to switch to that package, if I can't figure out how to do it without it.
yea, if you don't want to use additional libraries replace execute with your server action 👍
@Transvaal lion solved?
Transvaal lionOP
For now I went with useState instead of useOptimistic, but instead of executing the debounced server action in the useeffect hook, I did it in an onClick handler where I first updated the state, then called the debounced (useDebouncedCallback) server action. In the server action I did a revalidateTag on the cart or returned an error message, which I ended up handling in a useEffect where I checked the error state of useFormState and did the rollback in case of an error. Overall it works, but I wish I could have leveraged some work that somebody had already done on this. Can't believe people roll their own custom solution for such a normal thing 🙂
@Transvaal lion For now I went with useState instead of useOptimistic, but instead of executing the debounced server action in the useeffect hook, I did it in an onClick handler where I first updated the state, then called the debounced (useDebouncedCallback) server action. In the server action I did a revalidateTag on the cart or returned an error message, which I ended up handling in a useEffect where I checked the error state of useFormState and did the rollback in case of an error. Overall it works, but I wish I could have leveraged some work that somebody had already done on this. Can't believe people roll their own custom solution for such a normal thing 🙂
wow, that sounds pretty complicated. I would never use such a squishy solution, even if it's my own.

I like to say: you have to different ways to integrate a functionality:
1. Build it yourself.
2. Let other build it.

Advantages of 1.
- Full controll
- Perfect for your case
- No third party library, that can slow down

Disadvantages of 1.
- If you fuck up, your functionality slows down you app
- Time invest
- Brain invest

Advantages of 2.
- Secure solution and tested solution
- No time/brain invest
- Mostly controllable

Disadvantages of 2.
- Not fully controllable
- For most of the cases good
- Can slow down your app

I want to highlight the marked disadvantages. In both cases your app will get slower. But who do you trust more: you, that really can make a good functionality with your expirience right now. Or others with their secure and tested solution? 🤔
Transvaal lionOP
I'm not quite sure I follow. I just executed the debounced server action inside the onclick handler where you proposed it'd be done in a useeffect. Not a huge complexity difference there as I see it 🙂 Also I wish there would be some solution I could use so I didn't have to invent my own, but so far I haven't come across any - do you know of a library that makes this possible? I don't see how next safe action would really make it easier, since the whole reason I could not use useOptimistic was that it would not updte optimistically for at debounced server action. I assume next-safe-actions useOptimistic works the same way? Or did I misunderstand your suggested solution - I though you were proposing useState 🙂
... you proposed it'd be done in a useeffect. Not a huge complexity difference there as I see it
The difference that I see there is, that you create a server request each time and my example not 🙂

Also I wish there would be some solution I could use so I didn't have to invent my own
I already showed you one library 👍

I assume next-safe-actions useOptimistic works the same way?
It only updates the value clientside and makes the server action in the background and syncs the result after there is a result. Of course it does not debounce, because of that we do all the stuff around. As I said:
... that executes the server action in the background and directly updates the value. But there is no debounce in it.
Transvaal lionOP
No I'm using useDebounceCallback on the server action and calling that one inside the onClickHandler - this actually works as it should. The server actions is debounced fine. The problem basically just came down to that I had to use the onClick handler instead of form action attribute and then useState instead of Reacts useOptimistic, as it would not accept a debounced server action - it would only update for each server action that got initiated.

Maybe i'm misunderstanding you, but my problem with useOptimistic was that it required a server action to be initiated for each optimistic update. It was super nice that it would sync up afterwards, but it was unacceptable to initiate a server action request for each time I quickly clicked the increment quantity button. Therefore I wanted to debounce it, but that only meant the optimistic update wouldn't happen in between - only when the server action got triggered. I guess that is by design - it's not intended to be used for scenarios where you need debounce.

Your suggestion, while it might let you still useuseOptimistic, you would split up the call to useOptimistic and the server action, so how would the useOptimistic hook be able to roll back in case the server action failed or returned the previous value in case of an error? I don't see how you could utilize the built in rollback on error that justifies the useOptimistic hook?
I guess you know what you are doing.