DEV Community

Abhishek Arankal
Abhishek Arankal

Posted on

Building a Multi-Counter App in React — Improving Logic Step by Step

Introduction

To improve my logic-thinking and problem-solving skills, I decided to stop jumping directly into complex projects.
Instead, I started building small projects and improving them level by level, focusing more on logic than UI.

This article documents my journey of building a Multi-Counter Application in React, where each Git commit represents one learning.

The goal was simple:
Build logic incrementally, face real bugs, debug them, and learn deeply.

Project Overview

The Multi-Counter App allows users to:

  • Increment and decrement counters
  • Apply minimum and maximum constraints
  • Disable buttons based on logic
  • Reset counters

  • Correctly handle multiple counters without shared-state bugs

Each feature was added as a new level, not all at once.

Tech Stack

  • React.js
  • JavaScript (ES6)
  • Tailwind CSS
  • Git & GitHub

Level-by-Level Development 🚀

Level 1: Increment and Decrement
Commit: Level 1: Increment and Decrement
started with a basic counter using useState.

const [count, setCount] = useState(0)
const handleIncrement = () => setCount(count + 1);
const handleDecrement = () => setCount(count - 1);

Learning:

  • How React state updates work
  • How UI re-renders on state change

Level 2: Minimum Constraint & Disable Decrement Button
Commit: Level 2: Minimum Constraint and disabled button
I prevented the counter from going below 0 and disabled the decrement button when the value reached the minimum.

const handleDecrement = () => {
if(count > 0) {
setCount(count - 1)
}
}

<button disabled={count === 0}>Decrement</button>
Enter fullscreen mode Exit fullscreen mode

Learning:

  • Handling edge cases
  • Controlling UI with conditions

Level 3: Maximum Constraint & Disable Increment Button
Commit: Level 3: Maximum Constraint and Disable Increment Button
I added a maximum limit and disabled the increment button once the limit was reached.

const handleIncrement = () => {
if(count < 10){
setCount(count + 1)
}
}

<button disabled={count < 10}>Increment</button>
Enter fullscreen mode Exit fullscreen mode

Learning:

  • Defensive programming
  • Syncing UI with business logic

Level 4: Reset Functionality
Commit: Level 4: Reset Functionality
Added a reset button to bring the counter back to its initial value.

const handleReset = () => setCount(0);

<button className='px-4 py-2 bg-yellow-600 rounded hover:bg-yellow-700 transition' onClick={() => setCount(0)}>Reset</button>
Enter fullscreen mode Exit fullscreen mode

Learning:

  • Clean state reset patterns
  • Better user experience

Level 8: Fix Increment Logic for Multiple Counters
Commit: Level 8: Fix increment logic for multiple counters
This was the most important level.

When I introduced multiple counters, I initially faced serious logic bugs:

  • State updates behaved unpredictably
  • Incrementing one counter affected others

Debugging: counters.map is not a function in React
While working on the increment logic for multiple counters, I encountered a confusing runtime error:

Runtime Error: counters.map is not a function

Initial Implementation
Initially, my handleIncrement function looked like this:

const [counters, setCounters] = useState([
{id:1, value:0},
{id:2, value:0},
{id:3, value:0}
])
const handleIncrement = (id) => {
setCounters((prevCounters) => {
prevCounters.map((counter) => counter.id === id ? {...counter, value: counter.value < 10 ? counter.value + 1 : counter.value} : counter)
})
}

The Issue: counters.map is not a function
After clicking the increment or decrement button for the first time, the application crashed with the following error:
counters.map is not a function
This error did not happen during the click itself.
It occurred during the next render cycle, when React tried to re-render the UI.

What Happened Internally

  1. A button click triggered setCounters
  2. React updated the state
  3. React attempted to re-render the UI
  4. During rendering, this line executed: counters.map(...)
  5. At that moment, counters was no longer an array This caused the runtime error and broke the UI.

Root Cause
The problem was a missing return statement.

Because I used curly braces {} in the arrow function passed to setCounters, JavaScript does not return anything by default.

As a result:

  • The function returned undefined
  • React set counters state to undefined
  • On the next update, calling .map() caused the error

The Fix
I fixed the issue by explicitly returning the new array from map():

const handleIncrement = (id) => {
setCounters((prevCounters) => {
return prevCounters.map((counter) => counter.id === id ? {...counter, value: counter.value < 10 ? counter.value + 1 : counter.value} : counter)
})
}

Why This Works

  • map() returns a new array
  • return ensures React receives valid state
  • Only the clicked counter updates
  • State remains immutable

Core Logic Explained

const handleIncrement = (id) => {
setCounters((prevCounters) => {
return prevCounters.map((counter) => counter.id === id ? {...counter, value: counter.value < 10 ? counter.value + 1 : counter.value} : counter)
})
}

This logic:

  • Updates only one counter
  • Keeps others unchanged
  • Follows React’s immutability rules

Challenges Faced

  • State becoming undefined
  • Shared-state bugs with multiple counters
  • Forgetting how arrow functions return values

What I Learned

  • Most React bugs are JavaScript logic bugs
  • Missing return can silently break state
  • map is not a function usually means state is corrupted
  • Small projects expose real-world issues
  • Level-based learning builds confidence

Why I Used Level-Based Commits

  • Each commit = one learning
  • Easy to explain in interviews
  • Clear progress tracking
  • Meaningful GitHub activity

GitHub Repository

GitHub: https://github.com/Abhishek-Arankal/React-Counter

Final Thoughts

This project taught me one important lesson:
Logic improves through iteration, not complexity.

I’ll continue building small projects, documenting bugs, and learning in public.
If you see a better way to write this logic, I’d love your feedback.

Top comments (0)