title: React component sometimes renders twice with unchanged state
body: <p>I am using Redux to subscribe to a store and update a component.</p>
  
  <p>This is a simplified example without Redux. It uses a mock-up store to subscribe and dispatch to.</p>
  
  <p>Please, follow the steps below the snippet to reproduce the problem.</p>
  
  <p><strong>Edit</strong>: Please skip to the second demo snippet under <strong>Update</strong> for a more concise and closer to real-life scenario. The question is <em>not</em> about Redux. It's about React's setState function identity causing re-render in certain circumstances even though the state has not changed.</p>
  
  <p><strong>Edit 2</strong>: Added even more concise demo under "<strong>Update 2</strong>".</p>
  
  <p><div class="snippet" data-lang="js" data-hide="false" data-console="true" data-babel="true">
  <div class="snippet-code">
  <pre class="snippet-code-js lang-js prettyprint-override"><code>const {useState, useEffect} = React;
  
  let counter = 0;
  
  const createStore = () =&gt; {
   const listeners = [];
   
   const subscribe = (fn) =&gt; {
    listeners.push(fn);
    return () =&gt; {
     listeners.splice(listeners.indexOf(fn), 1);
    };
   }
   
   const dispatch = () =&gt; {
    listeners.forEach(fn =&gt; fn());
   };
   
   return {dispatch, subscribe};
  };
  
  const store = createStore();
  
  function Test() {
   const [yes, setYes] = useState('yes');
   
   useEffect(() =&gt; {
    return store.subscribe(() =&gt; {
     setYes('yes');
    });
   }, []);
   
   console.log(`Rendered ${++counter}`);
   
   return (
    &lt;div&gt;
     &lt;h1&gt;{yes}&lt;/h1&gt;
     &lt;button onClick={() =&gt; {
      setYes(yes === 'yes' ? 'no' : 'yes');
     }}&gt;Toggle&lt;/button&gt;
     &lt;button onClick={() =&gt; {
      store.dispatch();
     }}&gt;Set to Yes&lt;/button&gt;
    &lt;/div&gt;
   );
  }
  
  ReactDOM.render(&lt;Test /&gt;, document.getElementById('root'));</code></pre>
  <pre class="snippet-code-html lang-html prettyprint-override"><code>&lt;div id="root"&gt;&lt;/div&gt;
  &lt;script src="https://unpkg.com/react/umd/react.development.js"&gt;&lt;/script&gt;
  &lt;script src="https://unpkg.com/react-dom/umd/react-dom.development.js"&gt;&lt;/script&gt;</code></pre>
  </div>
  </div>
  </p>
  
  <h3>What is happening</h3>
  
  <ol>
  <li>✅ Click "Set to Yes". Since the value of <code>yes</code> is already "yes", state is unchanged, hence the component is not re-rendered.</li>
  <li>✅ Click "Toggle". <code>yes</code> is set to "no". State has changed, so the component is re-rendered.</li>
  <li>✅ Click "Set to Yes". <code>yes</code> is set to "yes". State has changed again, so the component is re-rendered.</li>
  <li>⛔ Click "Set to Yes" again. State has <strong>not</strong> changed, but the component <strong>is</strong> still re-rendered.</li>
  <li>✅ Subsequent clicks on "Set to Yes" do not cause re-rendering as expected.</li>
  </ol>
  
  <h3>What is expected to happen</h3>
  
  <p>On step 4 the component should not be re-rendered since state is unchanged.</p>
  
  <h1>Update</h1>
  
  <p>As the <a href="https://reactjs.org/docs/hooks-reference.html#timing-of-effects" rel="noreferrer">React docs</a> state, <code>useEffect</code> is </p>
  
  <blockquote>
    <p>suitable for the many common side effects, like setting up
    subscriptions and event handlers...</p>
  </blockquote>
  
  <p>One such use case could be listening to a browser event such as <code>online</code> and <code>offline</code>.</p>
  
  <p>In this example we call the function inside <code>useEffect</code> once when the component first renders, by passing it an empty array <code>[]</code>. The function sets up event listeners for online state changes.</p>
  
  <p>Suppose, in the app's interface we also have a button to manually toggle online state.</p>
  
  <p>Please, follow the steps below the snippet to reproduce the problem.</p>
  
  <p><div class="snippet" data-lang="js" data-hide="false" data-console="true" data-babel="true">
  <div class="snippet-code">
  <pre class="snippet-code-js lang-js prettyprint-override"><code>const {useState, useEffect} = React;
  
  let counter = 0;
  
  function Test() {
   const [online, setOnline] = useState(true);
   
   useEffect(() =&gt; {
    const onOnline = () =&gt; {
     setOnline(true);
    };
    const onOffline = () =&gt; {
     setOnline(false);
    };
    window.addEventListener('online', onOnline);
    window.addEventListener('offline', onOffline);
    
    return () =&gt; {
     window.removeEventListener('online', onOnline);
     window.removeEventListener('offline', onOffline);
    }
   }, []);
   
   console.log(`Rendered ${++counter}`);
   
   return (
    &lt;div&gt;
     &lt;h1&gt;{online ? 'Online' : 'Offline'}&lt;/h1&gt;
     &lt;button onClick={() =&gt; {
      setOnline(!online);
     }}&gt;Toggle&lt;/button&gt;
    &lt;/div&gt;
   );
  }
  
  ReactDOM.render(&lt;Test /&gt;, document.getElementById('root'));</code></pre>
  <pre class="snippet-code-html lang-html prettyprint-override"><code>&lt;div id="root"&gt;&lt;/div&gt;
  &lt;script src="https://unpkg.com/react/umd/react.development.js"&gt;&lt;/script&gt;
  &lt;script src="https://unpkg.com/react-dom/umd/react-dom.development.js"&gt;&lt;/script&gt;</code></pre>
  </div>
  </div>
  </p>
  
  <h3>What is happening</h3>
  
  <ol>
  <li>✅ The component is first rendered on the screen, and the message is logged in the console.</li>
  <li>✅ Click "Toggle". <code>online</code> is set to <code>false</code>. State has changed, so the component is re-rendered.</li>
  <li>⛔ Open Dev tools and in the Network panel switch to "offline". <code>online</code> was already <code>false</code>, thus state has <strong>not</strong> changed, but the component <strong>is</strong> still re-rendered.</li>
  </ol>
  
  <h3>What is expected to happen</h3>
  
  <p>On step 3 the component should not be re-rendered since state is unchanged.</p>
  
  <h1>Update 2</h1>
  
  <p><div class="snippet" data-lang="js" data-hide="false" data-console="true" data-babel="true">
  <div class="snippet-code">
  <pre class="snippet-code-js lang-js prettyprint-override"><code>const {useState, useEffect} = React;
  
  let counterRenderComplete = 0;
  let counterRenderStart = 0;
  
  
  function Test() {
    const [yes, setYes] = useState('yes');
  
    console.log(`Component function called ${++counterRenderComplete}`);
    
    useEffect(() =&gt; console.log(`Render completed ${++counterRenderStart}`));
  
    return (
      &lt;div&gt;
        &lt;h1&gt;{yes ? 'yes' : 'no'}&lt;/h1&gt;
        &lt;button onClick={() =&gt; {
          setYes(!yes);
        }}&gt;Toggle&lt;/button&gt;
        &lt;button onClick={() =&gt; {
          setYes('yes');
        }}&gt;Set to Yes&lt;/button&gt;
      &lt;/div&gt;
    );
  }
  
  ReactDOM.render(&lt;Test /&gt;, document.getElementById('root'));
  
   </code></pre>
  <pre class="snippet-code-html lang-html prettyprint-override"><code>&lt;div id="root"&gt;&lt;/div&gt;
  &lt;script src="https://unpkg.com/react/umd/react.development.js"&gt;&lt;/script&gt;
  &lt;script src="https://unpkg.com/react-dom/umd/react-dom.development.js"&gt;&lt;/script&gt;</code></pre>
  </div>
  </div>
  </p>
  
  <h3>What is happening</h3>
  
  <ol>
  <li>✅ Click "Set to Yes". Since the value of <code>yes</code> is already <code>true</code>, state is unchanged, hence the component is not re-rendered.</li>
  <li>✅ Click "Toggle". <code>yes</code> is set to <code>false</code>. State has changed, so the component is re-rendered.</li>
  <li>✅ Click "Set to Yes". <code>yes</code> is set to <code>true</code>. State has changed again, so the component is re-rendered.</li>
  <li>⛔ Click "Set to Yes" again. State has <strong>not</strong> changed, despite that the component starts the rendering process by calling the function. Nevertheless, React bails out of rendering somewhere in the middle of the process, and effects are not called.</li>
  <li>✅ Subsequent clicks on "Set to Yes" do not cause re-rendering (function calls) as expected.</li>
  </ol>
  
  <h3>Question</h3>
  
  <p>Why is the component still re-rendered? Am I doing something wrong, or is this a React bug?</p>