react icon indicating copy to clipboard operation
react copied to clipboard

Bug: Stale values returned from useOptimistic when state changes

Open dorshinar opened this issue 2 years ago • 4 comments

The optimisticState returned from useOptimistic is stale when the state param passed to useOptimistic is changed. The change could come from an RSC reponse or from a setState.

React version: 18.2.0

Steps To Reproduce

  1. Call useOptimistic with a state variable
  2. Update the value of state either via setState or any other method that would cause a rerender
  3. The initial state is returned as the optimisticState image

Link to code example: https://github.com/dorshinar/next-optimistic-bug

I've recreated the bug in a Next project, but the important component is this:

export function Form() {
  const [arrFromServer, setArrFromServer] = useState([1, 2, 3]);

  useEffect(() => {
    const interval = setInterval(() => {
      setArrFromServer((a) => [...a, a.length + 1]);
    }, 1000);

    return () => {
      clearInterval(interval);
    };
  }, []);

  const [optimisticArr] = useOptimistic(arrFromServer, (s) => s);
  console.log("🚀 ~ arrFromServer:", arrFromServer);
  console.log("🚀 ~ optimisticArr:", optimisticArr);

  return <div>{JSON.stringify(optimisticArr)}</div>;
}

The current behavior

When there is no form submission in progress, the optimisticArr always returns the initial state provided to it.

The expected behavior

I'd exepct that optimisticState returned from useOptimistic to be in sync with the state param passed to it.

dorshinar avatar Oct 28 '23 10:10 dorshinar

I'm experiencing the same issue.

magoz avatar Nov 23 '23 17:11 magoz

Me too. Downgrading to next 13 solves it for now

Feel-ix-343 avatar Nov 25 '23 18:11 Feel-ix-343

I'm experiencing the same issue.

katerynarieznik avatar Dec 04 '23 14:12 katerynarieznik

same issue here.

ipv4sq avatar Dec 09 '23 15:12 ipv4sq

same here, for me it sometimes flashes the correct value before changing back to the original value

Elee-Lawleit avatar Jan 10 '24 23:01 Elee-Lawleit

Same here. It's not more experimental version, why is this happening?

JovanJevtic avatar Jan 10 '24 23:01 JovanJevtic

same here, for me it sometimes flashes the correct value before changing back to the original value

For me it does every time

JovanJevtic avatar Jan 10 '24 23:01 JovanJevtic

https://github.com/facebook/react/assets/62719173/25ea9058-27fa-4367-bde8-3686f7d89c26

` 'use client'

import { addTodo } from "@/actions/todo"; import { Todo } from "@prisma/client"; import { ElementRef, useOptimistic, useRef } from "react"; import { v4 as uuid } from 'uuid' import AddButton from "./AddButton";

type Props = { todos: Todo[] }

const Form = ({ todos }: Props) => {

const formRef = useRef<ElementRef<'form'>>(null)

const [optimisticTodos, addOptimisticTodo] = useOptimistic<Todo[], string>(
    todos,
    (currTodos, newTodo: string) => {
        return [
            {
                text: newTodo,
                id: uuid(),
                createdAt: new Date(Date.now()),
                updatedAt: new Date(Date.now())
            },
            ...currTodos
        ]
    }  
)

return(
    <>
        <form
            ref={formRef}
            action={async formData => {
                formRef.current?.reset();

                addOptimisticTodo(formData.get("text") as string)

                await addTodo(formData)
            }}
            className="flex-row flex"
        >
            <label>Input text</label>
            <input 
                type="text"
                name="text"
                className="bg-blue-950"
            />
            <AddButton />
        </form>
        <ul className="mt-5">
            {
                optimisticTodos.map(todo => (
                    <li key={todo.id} className="mb-3">
                    <p>{todo.text}</p>
                    </li>
                ))
            }
        </ul>
    </>
);

}

export default Form `

JovanJevtic avatar Jan 10 '24 23:01 JovanJevtic

When there is no form submission in progress, the optimisticArr always returns the initial state provided to it.

I think this is an expected behaviour of useOptimistic. Or rather, the optimistic update is replaced with the new state as soon as the transition is over. When there is no async work in the transition, the optimistic update is replaced instantly.

Edit: In fact it says exactly that in the first line of the docs (emphasis my own):

useOptimistic is a React Hook that lets you show a different state while an async action is underway. It accepts some state as an argument and returns a copy of that state that can be different during the duration of an async action such as a network request.

It's expected for useOptimistic to essentially do nothing outside of async transitions.

tom-sherman avatar Jan 13 '24 23:01 tom-sherman

This is, indeed, a bug — I apologize for not looking into this until now (thanks @tom-sherman for the ping on ~Twitter~ X). I remember getting the ping from @sophiebits but forgot to follow up.

Should be fixed by https://github.com/facebook/react/pull/27936

acdlite avatar Jan 14 '24 00:01 acdlite

The fix has now landed in the most recent React and Next.js canaries. Thanks for the bug report!

acdlite avatar Jan 14 '24 17:01 acdlite