Search code examples
javascriptreactjsreact-hooksfrontendreact-state

How Can I Prevent Duplicate Behavior on prevState at counter in React?


I am completely new to React and trying to understand lessons related to prevState, particularly with counters. I tried to create buttons named by the counter number when clicking another button, but it's behaving strangely, and I don't know what's causing it.

enter image description here

Here is the part behaving strange.

const addButton = () => {  
  for (let i = 0; i < 5; i++) {
    console.log(`Iteration number: ${i + 1}`);
    setCounter(prevCounter => {
    const newButton = {
      id: `button${prevCounter}`,
      label: `Button ${prevCounter}`
    };
    
    setButtons(prevButtons => [...prevButtons, newButton]);
    return prevCounter +1;
  });
}};

All the code if need it.

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

function App() {
  const [Counter, setCounter] = useState(1);
  const [buttons, setButtons] = useState([]);

  const addButton = () => {    
    for (let i = 0; i < 5; i++) {
      console.log(`Iteration number: ${i + 1}`);
      setCounter(prevCounter => {
      const newButton = {
        id: `button${prevCounter}`,
        label: `Button ${prevCounter}`
      };
      
      setButtons(prevButtons => [...prevButtons, newButton]);
      return prevCounter +1;
    });
  }};

  return (
    <div className="p-4">
      <h1 className="text-2xl font-bold mb-4">Dynamic Button Creator</h1>
      <button 
        onClick={addButton} 
        className="bg-blue-500 text-white px-4 py-2 rounded mb-4"
      >
        Add Button
      </button>
      <div>
        {buttons.map(button => (
          <button
            key={button.id}
            id={button.id}
            onClick={addButton} 
            className="bg-green-500 text-white px-4 py-2 rounded mb-2 mr-2"
          >
            {button.label}
          </button>
        ))}
      </div>
    </div>
  );
}

export default App;

Solution

  • Issue

    The main issue stems from your use of a for-loop in the addButton callback to synchronously enqueue a bunch of React state updates and the app being rendered within a React.StrictMode component which double-invokes certain lifecycle methods and hook callbacks as a debugging tool to help surface bugs.

    See specifically Fixing bugs found by double rendering in development:

    React assumes that every component you write is a pure function. This means that React components you write must always return the same JSX given the same inputs (props, state, and context).

    Components breaking this rule behave unpredictably and cause bugs. To help you find accidentally impure code, Strict Mode calls some of your functions (only the ones that should be pure) twice in development. This includes:

    • Your component function body (only top-level logic, so this doesn’t include code inside event handlers)
    • Functions that you pass to useState, set functions, useMemo, or useReducer
    • Some class component methods like constructor, render, shouldComponentUpdate (see the whole list) If a function is pure, running it twice does not change its behavior because a pure function produces the same result every time. However, if a function is impure (for example, it mutates the data it receives), running it twice tends to be noticeable (that’s what makes it impure!) This helps you spot and fix the bug early.

    I've emphasized the bullet point relevant to your code. The setCounter callback isn't pure because it's calling the setButtons state updater.

    The state updaters are being called way more often than you expect, and then on subsequent updates are duplicating previous state values. This duplication leads to the duplicate React key issue since more than one button element has the same counter value used to compute the key.

    Solution

    If you are simply trying to just "add a button" each time the button is clicked then the following implementation should be sufficient:

    const addButton = () => {
      // Add a new button to the buttons array using the current counter value
      setButtons((buttons) =>
        buttons.concat({
          id: `button${counter}`,
          label: `Button ${counter}`,
        })
      );
    
      // Increment the counter value
      setCounter((counter) => counter + 1);
    };
    

    Demo

    function App() {
      const [counter, setCounter] = React.useState(1);
      const [buttons, setButtons] = React.useState([]);
    
      const addButton = () => {
        setButtons((buttons) =>
          buttons.concat({
            id: `button${counter}`,
            label: `Button ${counter}`,
          })
        );
        setCounter((counter) => counter + 1);
      };
    
      return (
        <div className="p-4">
          <h1 className="text-2xl font-bold mb-4">Dynamic Button Creator</h1>
          <button
            onClick={addButton}
            className="bg-blue-500 text-white px-4 py-2 rounded mb-4"
          >
            Add Button
          </button>
          <div>
            {buttons.map((button) => (
              <button
                key={button.id}
                id={button.id}
                onClick={addButton}
                className="bg-green-500 text-white px-4 py-2 rounded mb-2 mr-2"
              >
                {button.label}
              </button>
            ))}
          </div>
        </div>
      );
    }
    
    const rootElement = document.getElementById("root");
    const root = ReactDOM.createRoot(rootElement);
    
    root.render(
      <React.StrictMode>
        <App />
      </React.StrictMode>
    );
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
    <div id="root" />

    If you wanted to add multiple buttons per click then the loop should just wrap the above logic in a loop and still enqueue the state updates independently.

    const addButton = () => {
      // Start loop at current counter value, 
      // use loop counter i for button value
      for (let i = counter; i < counter + 3; i++) {
        setButtons((buttons) =>
          buttons.concat({
            id: `button${i}`,
            label: `Button ${i}`,
          })
        );
        setCounter((counter) => counter + 1);
      }
    };
    

    Demo 2

    function App() {
      const [counter, setCounter] = React.useState(1);
      const [buttons, setButtons] = React.useState([]);
    
      const addButton = () => {
        for (let i = counter; i < counter + 3; i++) {
          setButtons((buttons) =>
            buttons.concat({
              id: `button${i}`,
              label: `Button ${i}`,
            })
          );
          setCounter((counter) => counter + 1);
        }
      };
    
      return (
        <div className="p-4">
          <h1 className="text-2xl font-bold mb-4">Dynamic Button Creator</h1>
          <button
            onClick={addButton}
            className="bg-blue-500 text-white px-4 py-2 rounded mb-4"
          >
            Add 3 Buttons
          </button>
          <div>
            {buttons.map((button) => (
              <button
                key={button.id}
                id={button.id}
                onClick={addButton}
                className="bg-green-500 text-white px-4 py-2 rounded mb-2 mr-2"
              >
                {button.label}
              </button>
            ))}
          </div>
        </div>
      );
    }
    
    const rootElement = document.getElementById("root");
    const root = ReactDOM.createRoot(rootElement);
    
    root.render(
      <React.StrictMode>
        <App />
      </React.StrictMode>
    );
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
    <div id="root" />