React 18 Bug Use Suspense to load data,refresh for hundreds times
React version:18.1.0
(My English is poor...)
When i use to async load data,and reload the data through refresh and setRefresh flag,it appears to load hundreds of times,eventhough the data has already arrived.But if I use the flushAsync function,it performence correctly.So it seems because in React18,the flush behaviour changed,when in async and setTimeout,it only process once.
Steps To Reproduce
1.click the refresh button to refresh the data
Link to code example:https://codesandbox.io/s/distracted-wood-7gq0hd?file=/src/UI.jsx
Hi, I tried to reproduce the problem you described with minimal code.
import { useState, Suspense } from "react";
const Reader = ({ readable }) => readable.read();
class Readable {
pending = true;
value = 0;
promise = (async () => {
const value = await Promise.resolve(Math.random());
this.pending = false;
this.value = value;
})();
read() {
if (this.pending) {
throw this.promise;
}
return this.value;
}
}
export default () => {
const [, set] = useState(0);
const forceUpdate = () => set(x => x + 1);
const updateLater = () => {
setTimeout(() => {
forceUpdate();
}, 10);
};
const updateNow = () => {
forceUpdate();
};
const readable = new Readable()
console.log("Rendering");
return (
<>
<button type="button" onClick={updateLater}>
Update later
</button>
<button type="button" onClick={updateNow}>
Update now
</button>
<Suspense>
<Reader readable={readable} />
</Suspense>
</>
);
};
As you can see, triggering an update inside an event handler immediately is safe. But it goes wrong if I try to update the component in the callback of setTimeout()
. Throwing a difference promise in the tree of Suspend
might confuse React. But It seems like React can keep itself going into dead loop if the dead loop is started from event handlers.
A possible solution is trying to use a ref object to hold the readable
(The returned value from dataFetcher(lei.getData(id), setRefresh)
in your code). useMemo()
may also help.
Thanks to @j-sen ,I just tried to use useRef
and useMemo
,but it forms a closure,so i can't get the latest value.What i expected is using a class to control data alone,and use suspense
with React18 to fetch Data more graceful.I can initiative refetch the data from Internet to display the changed data an at the same time,I also can load the data from cache,So I want to know whether it's the wrong way I use it or the bug of react,why is there no problem when I use flushasync?
You are welcome. If the returned value from dataFetcher()
is wrapped in useMemo()
, the id
is supposed to be one of the dependencies.
const data = useMemo(() => {
return dataFetcher(lei.getData(id), setRefresh)
}, [id])
// skipping some lines
<UI data={data} fetcher={lei} />
If useRef()
is used, something like dataRef.current = dataFetcher(lei.getData(id), setRefresh)
should be done before calling setRefresh()
Using flushasync()
inside the timeout did help but I can't tell why because of my limited knowledge of React 18. I am not sure if this is a bug either.
I see. It seems that I didn't describe it clearly.Change the code to this doesn't meet my needs:
const data = useMemo(() => {
return dataFetcher(lei.getData(id), setRefresh);
console.log('rememo');
},[id]);
Because I want to retrieve the data without changing the id,and this doesn't work.So i need to add refresh
into the dependencies.
const data = useMemo(() => {
return dataFetcher(lei.getData(id), setRefresh);
console.log('rememo');
},[id,refresh]);
but there will be the bug of rendering repeatedly as before.So in my opion it's a bug of Suspense
.Anyway,thanks to you again!