6

The following code prints out the same time twice inside the console of codesandbox.io website (that version uses StrictMode) and also in the snippet below (not using StrictMode):

const { useState, useEffect } = React;

function useCurrentTime() {
  const [timeString, setTimeString] = useState("");

  useEffect(() => {
    const intervalID = setInterval(() => {
      setTimeString(new Date().toLocaleTimeString());
    }, 100);
    return () => clearInterval(intervalID);
  }, []);

  return timeString;
}

function App() {
  const s = useCurrentTime();
  console.log(s);

  return <div className="App">{s}</div>;
}

ReactDOM.render(<App />, document.getElementById("root"));
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.development.js"></script>

Demo: https://codesandbox.io/s/gallant-bas-3lq5w?file=/src/App.js (using StrictMode)

Here's a snippet using production libs; it still logs twice:

const { useState, useEffect } = React;

function useCurrentTime() {
  const [timeString, setTimeString] = useState("");

  useEffect(() => {
    const intervalID = setInterval(() => {
      setTimeString(new Date().toLocaleTimeString());
    }, 100);
    return () => clearInterval(intervalID);
  }, []);

  return timeString;
}

function App() {
  const s = useCurrentTime();
  console.log(s);

  return <div className="App">{s}</div>;
}

ReactDOM.render(<App />, document.getElementById("root"));
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>

However, when I open up the Developer's Console, I see each time printed out only once, and also in the codesandbox.io's console, I see it printed once.

And then if I create a standalone React app using create-react-app, and use the above code, and each time is printed twice.

How is this behavior understood, for printed out once or twice depending on different situations? My thinking was, if the state changes, then App is re-rendered, with that new string, once, so it is printed out once. What is strange is especially why does it printed out twice but when Dev Console is open, then it is once?

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
nonopolarity
  • 146,324
  • 131
  • 460
  • 740
  • All: This doesn't seem to be a `StrictMode` thing. – T.J. Crowder Mar 08 '21 at 07:48
  • Does this answer your question? https://stackoverflow.com/questions/61053432/react-usestate-cause-double-rendering @T.J.Crowder Are you sure, the linked codesandbox is rendered into a `StrictMode`, which seems would double-invoke the render and console log twice as a side-effect. – Drew Reese Mar 08 '21 at 07:48
  • @DrewReese - At first I assumed it was, but if I run the Stack Snippet above, which doesn't use `StrictMode`, I see the times logged twice....for a while, and then they just get logged once. – T.J. Crowder Mar 08 '21 at 07:50
  • @T.J.Crowder Ok, in the browser's console, yeah, I see the same. Did this behavior change in React v17? If I edit that same codesandbox and remove the `StrictMode` I still see the double-logging. If I apply the solution of console logging in an `useEffect` hook though I see only single logging, as I would expect. – Drew Reese Mar 08 '21 at 07:52
  • @DrewReese - I don't know. I even see it with the production libs. I feel like I'm being thick. :-) – T.J. Crowder Mar 08 '21 at 07:54
  • right, I removed the `StrictMode` and it was still twice, and also in the production build, supposedly it should not be rendered twice even with `StrictMode` there – nonopolarity Mar 08 '21 at 07:59
  • And the most peculiar thing is that if you navigate to a different tab and then come back to the codesandbox one, you see that the console.log was only printing once while you were away. At least in FF that seems to be the case. – codemonkey Mar 08 '21 at 08:00
  • If I change the timer callback so that it remembers the last time string and only calls `setTimeString` if it's a change, the behavior goes away. So it's calling `setTimeString` with the same string that's causing it...and it really shouldn't. (This is without `StrictMode` and using production, not development, libs.) There's definitely something involved here that I don't know about. I thought calling a state setter with the same value was effectively a no-op. – T.J. Crowder Mar 08 '21 at 08:02
  • 1
    @DrewReese - This being different for hooks in functional components would be **very** surprising. [The documentation](https://reactjs.org/docs/hooks-reference.html#usestate) seems to suggest that it isn't different: *"If your update function returns the exact same value as the current state, the subsequent rerender will be skipped completely."* Granted that's just talking about the functional update, but if I change the code to use a functional update, I still get the double logging. Also, if that were the issue, we'd be seeing ~10 updates, not just 2. – T.J. Crowder Mar 08 '21 at 08:25
  • @nonopolarity - I should note that the behavior I see here with the Stack Snippet doesn't match your description, but is surprising. I'm seeing two state updates...usually...when calling the state setter with the same value about ~10 times. V. strange. – T.J. Crowder Mar 08 '21 at 08:32
  • 1
    @T.J.Crowder Right, sorry, just tested this and you are correct, so I must certainly be mis-remembering the scenario I had. Deleting the comment but keeping what I believe still relevant: "I guess I neglected to look closely enough to the interval delay being used in OP's snippet and consider the granularity of the JS date object's locale string. Indeed, bumping the delay to 1000 I see only single logging." – Drew Reese Mar 08 '21 at 08:33
  • @DrewReese - It's all good -- we're all just trying to help. :-) Yeah, if you switch to 1s intervals, it goes away, although that brings its own issues (being up to nearly a second off, etc.), but for me the fundamental thing is that calling the state setter with the same value shouldn't be triggering a re-render. I've simplified it further without the custom hook and it still happens. o_O I mean, I get that React can re-render whenever it needs to, this just seems odd. – T.J. Crowder Mar 08 '21 at 08:50
  • 1
    @DrewReese - Just FYI, found the answer. – T.J. Crowder Mar 08 '21 at 10:02

1 Answers1

3

As far as I can tell, the double-call we see to App is expected behavior. From the useState documentation:

Bailing out of a state update

If you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)

Note that React may still need to render that specific component again before bailing out. That shouldn’t be a concern because React won’t unnecessarily go “deeper” into the tree. If you’re doing expensive calculations while rendering, you can optimize them with useMemo.

The key bits there are "Note that React may still need to render that specific component again before bailing out..." and "...React won’t unnecessarily go “deeper” into the tree..."

And indeed, if I update your example so that it uses a child component to show the time string, we only see that child component get called once per timeString value rather than twice as App is, even though the child component isn't wrapped in React.memo:

const { useState, useEffect } = React;

function useCurrentTime() {
    const [timeString, setTimeString] = useState("");
  
    useEffect(() => {
        const intervalID = setInterval(() => {
            setTimeString(new Date().toLocaleTimeString());
        }, 100);
        return () => clearInterval(intervalID);
    }, []);
  
    return timeString;
}

function ShowTime({timeString}) {
    console.log("ShowTime", timeString);
    return <div className="App">{timeString}</div>;
}

function App() {
    const s = useCurrentTime();
    console.log("App", s);
  
    return <ShowTime timeString={s} />;
}

ReactDOM.render(<App />, document.getElementById("root"));
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>

When I run that, I see:

App 09:57:14
ShowTime 09:57:14
App 09:57:14
App 09:57:15
ShowTime 09:57:15
App 09:57:15
App 09:57:16
ShowTime 09:57:16
App 09:57:16
App 09:57:17
ShowTime 09:57:17
App 09:57:17
App 09:57:18
ShowTime 09:57:18
App 09:57:18
App 09:57:19
ShowTime 09:57:19
App 09:57:19
App 09:57:20
ShowTime 09:57:20
App 09:57:20

Note how although App is called twice for each value of timeString, ShowTime is only called once.

I should note that this is more automatic than it was with class components. The equivalent class component would update 10 times per second if you didn't implement shouldComponentUpdate. :-)

Some of what you were seeing over on CodeSandbox may well have been due to StrictMode, more on that here. But the two calls to App for each timeString value are just React doing its thing.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • As to why React invokes it twice... could it be due to, if the interval or setTimeout is long, such as 33 or above, it shouldn't be a problem (Google Chrome has granularity of 4 or 5ms), but what if it is 0 or 2ms, and there are setInterval and clearInterval being triggered every time in the `useEffect()`? – nonopolarity Mar 08 '21 at 13:08
  • Come to think about it, if we switch to a different tab, then the console.log happens once only. So could it be that React is paranoid for some reason if the data is being displayed on screen, it can be wrong data, so it is doing it twice to make sure -- versus, if it is not displayed, but just setState and possibly just to trigger any effects or AJAX or anything like that, then it is safe to render the component only once? – nonopolarity Mar 08 '21 at 13:49
  • @nonopolarity - I suspect the reason you see only one call when you have switched away from the tab is that Chrome is throttling the interval timer, which Chrome does fairly aggressively. I don't think it has much if anything to do with the length of the interval, or with the minimum interval time. I think it's just that React calls your `App` function to be sure it doesn't render differently despite the same state value. Since your `App` doesn't, it doesn't call it again until the next actual state change. – T.J. Crowder Mar 08 '21 at 15:07