MassivePenguin
MassivePenguin

Reputation: 3511

React Hooks state variable not updated during setTimeout function

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

Answers (2)

ARZMI Imad
ARZMI Imad

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

Elisey
Elisey

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

Related Questions