MSOACC
MSOACC

Reputation: 3665

React state not updating as expected

I have a component that, when it is visible, I want to query an API every 10 seconds. When the component is closed / not visible, I want that polling to stop.

The problem I have is that the polling continues even after the component has been hidden. Whatever I am doing my code is not recognising the newly updated state variable.

const DisplayState = Object.freeze({
    DisplayGroupNamesForSelection: 0,
    DisplayLogStream: 1
});

export default function LogStream({ isOpen }) {
    const [currentState, setCurrentState] = useState(DisplayState.DisplayGroupNamesForSelection);
    const [logEvents, setlogEvents] = useState([]);

    const onStartStreamingButtonClicked = async logGroupName => {
        setCurrentState(DisplayState.DisplayLogStream);
        pollForLogs(logGroupName);
    };

    const pollForLogs = async logGroupName => {
        try {
            const newLogList = await getLogs(logGroupName); // API call elsehwere
            setlogEvents(newLogList);
        } catch(e) {
            //
        } finally {
            // If the component is still open, ping the API again in 10s
            // However, this always evaluates to true
            if(isOpen) {
                setTimeout(() => {
                    pollForLogs(logGroupName);
                }, 10000);
            }
        }
    };

    return (
        <>
            {!isOpen ? (
                <></>
            ) : (
                <div>
                    {(() => {
                        switch(currentState) {
                            case DisplayState.DisplayGroupNamesForSelection: {
                                return (
                                    <LogStreamingGroupSelector
                                        onStartStreamingButtonClicked={onStartStreamingButtonClicked}
                                    />
                                );
                            }
                            case DisplayState.DisplayLogStream: {
                                return (
                                    <div>
                                        {logEvents.map((log, i) => (
                                            <sub key={log.timestamp}>{log.message}</sub>
                                        ))}
                                    </div>
                                );
                            }
                        }
                    })()}
                </div>
            )}
        </>
    );
}

What I can do is a kinda' hacky solution where I declare a variable outside the component and have the continue polling depend on that:

let continuePollingForLogs = false;

export default function LogStream({ isOpen }) {
    const [currentState, setCurrentState] = useState(DisplayState.DisplayGroupNamesForSelection);
    const [logEvents, setlogEvents] = useState([]);

    useEffect(() => {
        continuePollingForLogs = isOpen;
    }, [isOpen]);

...and then if I have the pollForLogs function depend on the non-state variable continuePollingForLogs, it does behave as intended.

I know something is going wrong here with how React handles state, but what exactly I cannot figure out. Even when I created independent useState variables which I set within a useEffect whenever isOpen changed it still didn't work. The variable in pollForLogs()'s finally was always true.

Upvotes: 0

Views: 197

Answers (2)

MSOACC
MSOACC

Reputation: 3665

This is the wrong approach when using hooks. I suggest any future reader read Dan Abramov's article and use his useInterval custom hook:

https://overreacted.io/making-setinterval-declarative-with-react-hooks/

Upvotes: 1

Kasi
Kasi

Reputation: 747

You could use the useRef to create a mutable ref (variable) whose state (value) will be persisted throughout the component's lifecycle. Then, use the cleanup function in useEffect to set it to false when component unmounts.

import React, { useState, useRef } from 'react';

export default function LogStream({ isOpen }) {
  const [currentState, setCurrentState] = useState(
    DisplayState.DisplayGroupNamesForSelection
  );
  const [logEvents, setlogEvents] = useState([]);

  const continuePollingForLogs = useRef(isOpen);

  useEffect(() => {
    return () => {
      // set ref to false when component unmounts.
      continuePollingForLogs.current = false;
    };
  });
}

Upvotes: 2

Related Questions