facebook / react

The library for web and native user interfaces.
https://react.dev
MIT License
229.36k stars 46.97k forks source link

Bug: optimistic state (useOptimistic) shows both optimistic and returned from server data when running several async actions #28574

Open acherkashin opened 8 months ago

acherkashin commented 8 months ago

I have a form with submit button. When I click on the button, I call update optimistic state and update real state as soon as action is finished. When I click on the button 3 times, I will see 6 items in the list for a while.

https://github.com/facebook/react/assets/9947582/234d8e6c-a69b-429a-bc89-92dc4818f8db

React version:

Steps To Reproduce

  1. Click 3 times on the button

Link to repository: https://github.com/acherkashin/react19-useOptimistic

Link to sandbox https://stackblitz.com/~/github.com/acherkashin/react19-useOptimistic

const AddToCartForm = ({ title, addToCart, optimisticAddToCart }: AddToCartFormProps) => {
  const formAction = async (formData: FormData) => {
    const itemId = String(formData.get('itemID'));
    optimisticAddToCart({ id: itemId, title, pending: true });
    try {
      await addToCart(itemId, title);
    } catch (e) {
      console.log(e);
      // show error notification
    }
  };

  return (
    <form action={formAction}>
      <h2>{title}</h2>
      <input type="hidden" name="itemID" value={crypto.randomUUID()} />
      <button type="submit">Add to Cart</button>
    </form>
  );
};

The current behavior

I see every added item 2 times.

The expected behavior

If I add 3 elements, I should see 3 items in the list, not 6.

eps1lon commented 8 months ago

Thank you for the comprehensive repro.

I reduced it a bit more and also added two potential workarounds that I highlighted in comments as "Alternate A" and "Alternate B" as well as one with (in my opinion) degraded UX in "Alternate C": https://stackblitz.com/edit/react-9vvm42?file=src%2FApp.js

First two clicks create just one pending update. Everything works fine.

Last click happens while the action from the prior click is still pending, creating this intermediate state where an item is both in the optimistic and non-optimistic slice.

Screencast from 18.03.2024 11:01:08.webm

I also added a test that repros the original issue: https://github.com/facebook/react/pull/28575

However, I'll double check with the team if this shouldn't have "just worked" as you authored it. If either of the workarounds are required, we should definitely document it. Especially since it's not quite clear to me how this would work with primitives e.g. incrementing an optimistic click counter.

JSerZANP commented 8 months ago

FYI, This could be reproed in the official demo

https://github.com/facebook/react/assets/69352453/b596a882-302e-45bc-be3f-d64e85188571

acherkashin commented 8 months ago

@eps1lon Your "Alternate A" solution is pretty clear and works great.

However, I have a questions about "Alternate B" solution. Why optimistic update function is called 3 times every time optimistic state is updated?

image

And how this solution work at all, I mean, how item can be inside non-optimistic state earlier optimistic version if we append it to optimistic version first? 🤔

eps1lon commented 8 months ago

Why optimistic update function is called 3 times every time optimistic state is updated?

When an action completes, we rebase the optimistic updates (i.e. replay) on top of the passthrough value (the non-optimistic state). That's why you see multile calls.

acherkashin commented 8 months ago

@eps1lon thank you for the explanation.

And how this solution work at all, I mean, how item can be inside non-optimistic state earlier optimistic version if we append it to optimistic version first? 🤔

Could you throw the light on the question above?

eps1lon commented 8 months ago

Because we added it to the non-optimistic slice but it's still in the optimistic slice because one action is still pending.

cjg1122 commented 3 months ago

Merging optimistically updated values by unique IDs?