Reputation: 3511
I have a page that's updating a mock 'console' with log messages pulled from an array. The way it's supposed to work is that every n seconds a new message is added to the log.
Simple, right? Just use a setTimout
function. Not with Hooks!
Here's some (abridged) code:
const [logIndex, setLogIndex] = useState(0);
const [logs, setLogs] = useState([]);
let interval = useRef(setTimeout(() => {}, 0);
useEffect(() => {
interval.current = setTimeout(addNextLogMessage, 2000);
return(() => {
clearTimeout(interval.current); // cleanup
}
}, []); // run when page first loads
const sourceLogs = [
<p>Message 1</p>,
<p>Message 2</p>,
<p>Message 3</p>,
<p>Message 4</p>,
<p>Message 5</p>,
<p>Message 6</p>
];
const timeBetweenMessages = Math.ceil((1000 * 60 * 3) / sourceLogs.length); // three minutes (1000ms * 60sec * 3min) divided into number of log messages, rounded up
const addNextLogMessage () => {
setLogIndex(logIndex => logIndex + 1);
clearTimeout(interval.current);
if(logIndex < sourceLogs.length) {
setLogs(logs => [...logs, {...sourceLogs[logIndex], timeStamp: new Date().toString()}]);
interval.current = setTimeout(addNextLogMessage, Math.random() * timeBetweenMessages + 500);
}
}
return (
<>
{
logs.map(item => item);
}
</>
);
I'm obviously doing something dumb here, as the value of logIndex is always 0. Does anyone have any pointers?
Upvotes: 2
Views: 2712
Reputation: 980
You're code was correct, you just have to add logIndex,logs in use Effect bracket like below , so the addNextLogMessage will reference the new state.
In each time the state is updated a new addNextLogMessage is created, if you do not updated your interval with new one(addNextLogMessage ), it will still referencing the first one which having logIndex equal to 0 so it will always display "message 1"
import React, { useState, useRef, useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const [logs, setLogs] = useState([]);
const [timeOut, setTimeOut] = useState(2000);
const interval = useRef(setTimeout(() => {}, 0));
const [logIndex, setLogIndex] = useState(0);
useEffect(() => {
interval.current = setTimeout(addNextLogMessage, timeOut);
return () => {
clearTimeout(interval.current); // cleanup
};
}, [logIndex, logs]);
const sourceLogs = [
<p>Message 1</p>,
<p>Message 2</p>,
<p>Message 3</p>,
<p>Message 4</p>,
<p>Message 5</p>,
<p>Message 6</p>
];
const timeBetweenMessages = Math.ceil((1000 * 60 * 3) / sourceLogs.length);
const addNextLogMessage = () => {
setTimeOut(Math.random() * timeBetweenMessages + 500);
setLogIndex((logIndex) => logIndex + 1);
if (logIndex < sourceLogs.length) {
setLogs((logs) => [
...logs,
{ ...sourceLogs[logIndex], timeStamp: new Date().toString() }
]);
}
};
return <>{logs.map((item) => item)}</>;
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
test it here
Upvotes: 1
Reputation: 147
I made a solution with codesandbox for you. Does this behavior was required? I just followed console warning advices and that's it:
https://codesandbox.io/s/late-tree-01bqh?file=/src/App.js
Here is the code from it:
import React, {
useMemo,
useCallback,
useState,
useRef,
useEffect
} from "react";
import "./styles.css";
export default function App() {
const [logIndex, setLogIndex] = useState(0);
const [logs, setLogs] = useState([]);
const sourceLogs = useMemo(
() => [
<p>Message 1</p>,
<p>Message 2</p>,
<p>Message 3</p>,
<p>Message 4</p>,
<p>Message 5</p>,
<p>Message 6</p>
],
[]
);
let interval = useRef(setTimeout(() => {}, 0));
const timeBetweenMessages = Math.ceil((1000 * 60 * 3) / sourceLogs.length); // three minutes (1000ms * 60sec * 3min) divided into number of log messages, rounded up
const addNextLogMessage = useCallback(() => {
setLogIndex((logIndex) => logIndex + 1);
clearTimeout(interval.current);
if (logIndex < sourceLogs.length) {
setLogs((logs) => [
...logs,
{ ...sourceLogs[logIndex], timeStamp: new Date().toString() }
]);
interval.current = setTimeout(
addNextLogMessage,
Math.random() * timeBetweenMessages + 500
);
}
}, [logIndex, sourceLogs, timeBetweenMessages]);
useEffect(() => {
interval.current = setTimeout(addNextLogMessage, 2000);
return () => {
clearTimeout(interval.current); // cleanup
};
}, [addNextLogMessage]);
return (
<>
{logs.map((item, i) => (
<div key={`${i}-item`}>{item}</div>
))}
</>
);
}
Upvotes: 1