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.
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;
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
, oruseReducer
- 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.
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);
};
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);
}
};
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" />