问题
Context:
- New messages are added (e.g. every two seconds, using setInterval).
- Messages have status, either old or new. Newly added messages have a 'new' flag.
- After every 5 seconds, all 'new' messages are designated 'old'. (setTimeout)
Problem:
- Timeout is not triggering until the end. New messages are added, but they remain 'new' until all messages are added.
- I suspect that after every update the timeout is being reset/cleared and because the updates are occurring faster than the timeout, then the timeout callback never triggers in time (as a result only the final timeout get's triggered).
function useInterval(callback, delay) {
const savedCallback = React.useRef();
// Remember the latest callback.
React.useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
React.useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
const defaultMessages = [
{
message: "message 1",
new: false
},
{
message: "message 2",
new: false
},
{
message: "message 3",
new: true
}
];
export default function App() {
const [messages, setMessages] = React.useState(defaultMessages);
const messagesRef = React.useRef(messages);
messagesRef.current = messages;
// add a new message every 2 seconds
useInterval(() => {
messages.length < 10 &&
setMessages([
...messages,
{ message: `message ${messages.length + 1}`, new: true }
]);
}, 2000);
React.useEffect(() => {
const timer = setTimeout(() => {
// console.log("change all messages from new to old");
const updateMessages = messagesRef.current.map(m => ({
...m,
new: false
}));
setMessages([...updateMessages]);
}, 5000); // if you change this to duration less than 2 seconds then it runs just fine
return () => clearTimeout(timer); // removing the timer, calls it with every message update and seemingly ignores the timeout duration
});
return (
<div className="App">
{messages.map(m => (
<div key={m.message}>
{m.message}, status: {m.new ? "new" : "old"}
</div>
))}
</div>
);
}
Example code: https://codesandbox.io/s/settimeout-resetting-with-updates-ufl3b
Not sure how to approach this with React Hooks api. The timeouts need to persist, five seconds after every update. Where as right now every timeout seemed to be canceled or queued by the one that comes after it. I'm puzzled.
Thank you!
回答1:
Well, the main problem that I noticed is clearing the timeout on next render, meaning, if you render faster enough, you actually canceling the timeout callback instead of running it.
React.useEffect(() => {
const timer = setTimeout(() => {});
// will clear the timeout on ***next*** render!
return () => clearTimeout(timer);
});
So after fixing it, and using functional updates instead of reference, seems like this code works:
export default function App() {
const [messages, setMessages] = React.useState(defaultMessages);
// add a new message every 2 seconds
useInterval(() => {
messages.length < 10 &&
setMessages(prev => [
...prev,
{ message: `message ${messages.length + 1}`, new: true }
]);
}, 2000);
React.useEffect(() => {
console.log("rendered");
setTimeout(() => {
setMessages(prev =>
prev.map(m => ({
...m,
new: false
}))
);
}, 3000);
});
return (
<div className="App">
{messages.map(m => (
<div key={m.message}>
{m.message}, status: {m.new ? "new" : "old"}
</div>
))}
</div>
);
}
来源:https://stackoverflow.com/questions/62641564/react-hooks-settimeout-in-useeffect-not-triggering-until-end-because-of-state