Mini Project: Interactive Counter with Step Controls0%
Mini Project: Interactive Counter with Step Controls
Beginner12 min read•Updated: Jul 11, 2026
Mini Project: Interactive Counter with Step Controls
1. Project Overview & Requirements
In this hands-on mini-project, we will synthesize the foundational concepts of React—including useState, event handling, controlled inputs, and conditional rendering—into a full-featured, accessible Interactive Counter with Step Controls.
Requirements:
- 1Value Management: Maintain a current integer counter value.
- 2Step Increment / Decrement: Allow users to choose their step magnitude (e.g. step by 1, 5, 10, or custom).
- 3Boundaries & Limits: Enforce minimum (e.g. -50) and maximum (e.g. 100) boundaries with disabled button states.
- 4History Log: Keep a chronological log of previous actions (timestamp, action type, resulting value).
- 5Reset Capability: Reset the counter to 0 and clear history.
Output
┌────────────────────────────────────────────────────────┐
│ Interactive Counter Utility │
│ │
│ [ 45 ] │
│ │
│ Step Size: [ 5 ] │
│ [ - Decrement ] [ + Increment ] [ Reset ] │
│ │
│ Audit History: │
│ • 14:02:10 - Incremented by 5 (New Value: 45) │
│ • 14:02:08 - Incremented by 5 (New Value: 40) │
└────────────────────────────────────────────────────────┘
2. Component Implementation
JSX
import React, { useState } from 'react';
const MIN_VALUE = -50;
const MAX_VALUE = 100;
export default function StepCounter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
const [history, setHistory] = useState([]);
// Helper to log actions
const logAction = (action, prevVal, nextVal) => {
const entry = {
id: Date.now(),
time: new Date().toLocaleTimeString(),
description: `${action} (from ${prevVal} to ${nextVal})`
};
setHistory((prev) => [entry, ...prev.slice(0, 9)]); // Keep last 10 entries
};
// Handlers
const handleIncrement = () => {
setCount((prev) => {
const next = Math.min(prev + step, MAX_VALUE);
logAction(`Incremented by ${step}`, prev, next);
return next;
});
};
const handleDecrement = () => {
setCount((prev) => {
const next = Math.max(prev - step, MIN_VALUE);
logAction(`Decremented by ${step}`, prev, next);
return next;
});
};
const handleReset = () => {
logAction('Reset to zero', count, 0);
setCount(0);
};
const handleStepChange = (e) => {
const parsed = parseInt(e.target.value, 10);
setStep(isNaN(parsed) || parsed < 1 ? 1 : parsed);
};
const isAtMin = count <= MIN_VALUE;
const isAtMax = count >= MAX_VALUE;
return (
<div className="counter-container">
<h2>Interactive Step Counter</h2>
{/* Main Display */}
<div className={`count-badge ${count > 0 ? 'positive' : count < 0 ? 'negative' : 'neutral'}`}>
<span className="count-number">{count}</span>
</div>
{/* Limit Warnings */}
{isAtMax && <p className="warning-text">Reached maximum limit of {MAX_VALUE}!</p>}
{isAtMin && <p className="warning-text">Reached minimum limit of {MIN_VALUE}!</p>}
{/* Controls */}
<div className="controls-box">
<label>
Step Size:
<input
type="number"
min="1"
max="25"
value={step}
onChange={handleStepChange}
className="step-input"
/>
</label>
<div className="button-row">
<button onClick={handleDecrement} disabled={isAtMin} className="btn btn-secondary">
- {step}
</button>
<button onClick={handleReset} className="btn btn-outline">
Reset
</button>
<button onClick={handleIncrement} disabled={isAtMax} className="btn btn-primary">
+ {step}
</button>
</div>
</div>
{/* Audit History Log */}
{history.length > 0 && (
<div className="history-section">
<h4>Recent Activity</h4>
<ul className="history-list">
{history.map((item) => (
<li key={item.id}>
<span className="timestamp">[{item.time}]</span> {item.description}
</li>
))}
</ul>
</div>
)}
</div>
);
}
3. Key Architecture Concepts Illustrated
- 1Functional State Updates:
setCount(prev => ...)ensures accurate arithmetic even during rapid user clicks. - 2Immutable Array History:
setHistory(prev => [entry, ...prev])prepends new log entries immutably without mutating previous history. - 3Derived Boundary Disabling:
isAtMinandisAtMaxare computed values during render, cleanly disabling buttons when limits are reached.
Interactive Quiz
Practice Quiz
Question 1
Why is
Math.min(prev + step, MAX_VALUE) used when incrementing the counter?Question 2
How does the component limit the history log to the most recent 10 events?
Question 3
What is the benefit of computing
isAtMin = count <= MIN_VALUE during render instead of storing isAtMin in a separate useState?Question 4
Why is
parseInt(e.target.value, 10) paired with isNaN checking in handleStepChange?Question 5
What HTML attribute is used to disable the increment button when the maximum threshold is reached?
Next Lesson
Mini Project: Dynamic FAQ Accordion
Continue learning with hands-on practice, examples, and exercises in the upcoming topic.
Related Lessons
| Previous Lesson | Next Lesson |
|---|---|
| Utility-First Styling with Tailwind CSS in React | Mini Project: Dynamic FAQ Accordion |
Practice Quiz
Test your understanding of this lesson with 5 questions. Each question has one correct answer.