0

Edit: This issue was caused by React.StrictMode, but I am still unsure as to what the proper solution for my underlying problem should be. I need to perform an action with side effects based on a state change cause by an event, but I need more context than just the new value to decide if the side effect should happen, and the context only exists within the event handler.


I could not find any mention of this peculiar behaviour on Stack Overflow, but it seems like something many people must have encountered before.

Problem

When modifying a state created using the useState hook, calling fetch or alert from inside the setState callback results in these functions being called more than once, while everything else in the callback is executed just once.

At first, I thought the event was being fired repeatedly, but that is clearly not the case, as demonstrated by the surrounding console.log calls, all of which will only be called once. Then, I thought maybe the callback was being call repeatedly, but even that cannot be true, once again proven by the surrounding console.log calls.

Motivation

I understand that this is most likely an illegal use of the setState, but I am now curious about what causes this strange behaviour.

The reason why I wrote my code this way was because I needed the new state value in an API request that was sent under certain conditions.

I read that it is best to use the callback version of setState when modifying the state based on its previous value, so that is what I did. However, I also needed to make the API request with the new value, and I could not simply perform the same operation on the current state value outside the callback because it could not be guaranteed that it would be the same value that the callback was going to generate (or at least that is what I read about setState).

Here is a SO answer suggesting my approach, but they do not explicitly mention that the setState callback was a suitable place for side effects. And here is a SO answer with a possible solution/workaround to my problem.

I suppose I could use useEffect with a dependency set to the state variable to achieve the same result, but I need more than just the new value; I also need to know what conditions have caused the value to change, which makes things more complicated than the example in the linked question.

Example

This is the App.tsx file. The rest was generated using Create React App (npm init react-app my-app) and has not been modified.

When you load the page and press just a or b for the first time, everything seems fine, but if you type anything else before pressing a or b, the fetch or alert functions, respectively, are called (at least) twice.

import React, { useState } from "react";
import "./App.css";

function App() {
    const [typedText, setTypedText] = useState<string>("");

    function keyDownHandler(event: React.KeyboardEvent<HTMLInputElement>) {
        console.log(`keyDownHandler: ${event.key}`);
        if (event.key === "a") {
            setTypedText(text => {
                console.log("before fetch");

                fetch("https://jsonplaceholder.typicode.com/todos/1")
                    .then(res => console.log("fetch"))
                    .catch(console.error)
                    .finally(() => console.log("finally"));

                console.log("after fetch");

                return text + " ";
            });
        } else if (event.key === "b") {
            setTypedText(text => {
                console.log("before alert");
                alert("Hello World!");
                console.log("after alert");

                return text + " ";
            });
        } else {
            setTypedText(text => text + event.key);
        }
    }

    return (
        <div className="App">
            <div>{typedText}</div>
            <input type="text" onKeyDown={keyDownHandler} autoFocus />
        </div>
    );
}

export default App;

Here is an example of a log generated using the code above to illustrate my point, and to save you the time necessary to install and run all the Node magic.

App.tsx:8 keyDownHandler: a
App.tsx:11 before fetch
App.tsx:18 after fetch
App.tsx:13 fetch
App.tsx:13 finally
App.tsx:8 keyDownHandler: s
App.tsx:8 keyDownHandler: a
App.tsx:11 before fetch
App.tsx:18 after fetch
App.tsx:13 fetch
App.tsx:13 finally
App.tsx:13 fetch
App.tsx:13 finally
App.tsx:8 keyDownHandler: b
App.tsx:24 before alert
<Alert appears>
<Alert appears immediately again>
App.tsx:26 after alert
natiiix
  • 1,005
  • 1
  • 13
  • 21
  • Check if your app is using React's strict mode. If so, try disabling it, see if it results in a difference. – CertainPerformance Dec 18 '21 at 18:11
  • @CertainPerformance Oooh, alright, I had it enabled for the entire app, but I assume that was a good thing, right? So I should not solve this problem by just disabling it, but rather find an actual solution. – natiiix Dec 18 '21 at 18:17
  • Yeah. Could be that the issue is the `fetch` with a side-effect inside state setter functions. – CertainPerformance Dec 18 '21 at 18:20
  • @CertainPerformance It makes perfect sense since strict mode causes double-render, but why would it only affect `fetch` and `alert`, but had no effect on `console.log`? Surely enough, it also has a side effect. – natiiix Dec 18 '21 at 18:22
  • See https://reactjs.org/docs/strict-mode.html, React disables `console.log` on the duplicate render – CertainPerformance Dec 18 '21 at 18:23
  • @CertainPerformance Damn... Thank you for the speedy replies! This bizarre combination of things I was completely oblivious to has been driving me absolutely crazy for the past many hours. – natiiix Dec 18 '21 at 18:25
  • @natiiix solved? – NiRUS Dec 18 '21 at 18:46
  • @NiRUS Please see my edit at the top of the question. While this technically answers the mysterious part of the issue, I am not really sure what the correct solution should be. I cannot just use `useEffect` since I also need to have some extra context from the event handler. – natiiix Dec 18 '21 at 18:47

0 Answers0