useCounter
Manage counter state with increment, decrement, and reset functionality.
A simple yet powerful hook for managing numeric state. It handles common counter operations like incrementing, decrementing, resetting, and step-based updates.
Source Code
View the full hook implementation in the Hook Source Code section below.
Basic Counter
A simple counter with increment, decrement, and reset functionality.
0
"use client";
import { useCounter } from "@repo/hooks/utility/use-counter";
import { Button } from "@repo/ui/components/button";
import { Plus, Minus, RotateCcw } from "lucide-react";
export const Example1 = () => {
const { count, increment, decrement, reset } = useCounter(0, { step: 1 });
return (
<div className="flex flex-col items-center gap-4 rounded-lg border p-6">
<div className="text-4xl font-bold tabular-nums">{count}</div>
<div className="flex gap-2">
<Button
variant="outline"
size="icon"
onClick={decrement}
aria-label="Decrement"
>
<Minus className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
onClick={reset}
aria-label="Reset"
>
<RotateCcw className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
onClick={increment}
aria-label="Increment"
>
<Plus className="h-4 w-4" />
</Button>
</div>
</div>
);
};
Quantity Selector
Using limits to create a robust quantity selector for e-commerce.
Coffee Beans
$12.00 / bag
1
Total: $12.00
Max 10 items
"use client";
import { useCounter } from "@repo/hooks/utility/use-counter";
import { Button } from "@repo/ui/components/button";
import { ShoppingCart, Plus, Minus } from "lucide-react";
export const Example2 = () => {
// Quantity selector usually starts at 1, step 1, min 1, max 10
const { count, increment, decrement, canIncrement, canDecrement } =
useCounter(1, {
min: 1,
max: 10,
});
return (
<div className="w-full max-w-sm rounded-lg border p-4 shadow-sm">
<div className="mb-4 flex items-center justify-between">
<div>
<h3 className="font-medium">Coffee Beans</h3>
<p className="text-muted-foreground text-sm">
$12.00 / bag
</p>
</div>
<div className="bg-primary/10 text-primary rounded-full p-2">
<ShoppingCart className="h-5 w-5" />
</div>
</div>
<div className="flex items-center justify-between border-t pt-4">
<div className="flex items-center gap-3">
<Button
variant="outline"
size="icon"
className="h-8 w-8"
onClick={decrement}
isDisabled={!canDecrement}
>
<Minus className="h-3 w-3" />
</Button>
<span className="w-8 text-center font-medium tabular-nums">
{count}
</span>
<Button
variant="outline"
size="icon"
className="h-8 w-8"
onClick={increment}
isDisabled={!canIncrement}
>
<Plus className="h-3 w-3" />
</Button>
</div>
<div className="font-medium">
Total: ${(count * 12).toFixed(2)}
</div>
</div>
<p className="text-muted-foreground mt-2 text-right text-xs">
Max 10 items
</p>
</div>
);
};
Pagination Controls
Implementing pagination logic using incrementBy and decrementBy.
Page 1 of 20
Item 1
Item 2
Item 3
Item 4
Item 5
1
"use client";
import { useCounter } from "@repo/hooks/utility/use-counter";
import { Button } from "@repo/ui/components/button";
import {
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
} from "lucide-react";
export const Example3 = () => {
// Pagination: Start at page 1, total pages = 20
const totalPages = 20;
const {
count: page,
increment,
decrement,
reset: goToFirst,
incrementBy,
} = useCounter(1, { step: 1 });
// Helpers to keep within bounds
const nextPage = () => {
if (page < totalPages) increment();
};
const prevPage = () => {
if (page > 1) decrement();
};
const goToLast = () => {
const diff = totalPages - page;
if (diff > 0) incrementBy(diff);
};
// Simulating fetching data for current page
const items = Array.from({ length: 5 }, (_, i) => ({
id: (page - 1) * 5 + i + 1,
name: `Item ${(page - 1) * 5 + i + 1}`,
}));
return (
<div className="w-full max-w-md space-y-4">
{/* Limit simulation */}
<div className="rounded-lg border bg-zinc-50 p-4 dark:bg-zinc-900">
<h4 className="mb-2 text-sm font-medium text-zinc-500">
Page {page} of {totalPages}
</h4>
<div className="space-y-1">
{items.map((item) => (
<div
key={item.id}
className="rounded bg-white p-2 text-sm shadow-sm dark:bg-zinc-800"
>
{item.name}
</div>
))}
</div>
</div>
{/* Controls */}
<div className="flex items-center justify-center gap-1">
<Button
variant="ghost"
size="icon"
onClick={goToFirst}
isDisabled={page === 1}
aria-label="First page"
>
<ChevronsLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
onClick={prevPage}
isDisabled={page === 1}
aria-label="Previous page"
>
<ChevronLeft className="h-4 w-4" />
</Button>
<div className="mx-2 min-w-[3ch] text-center text-sm font-medium">
{page}
</div>
<Button
variant="outline"
size="icon"
onClick={nextPage}
isDisabled={page === totalPages}
aria-label="Next page"
>
<ChevronRight className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={goToLast}
isDisabled={page === totalPages}
aria-label="Last page"
>
<ChevronsRight className="h-4 w-4" />
</Button>
</div>
</div>
);
};
API Reference
Hook Signature
function useCounter(
initialCount?: number,
options?: UseCounterOpts,
): UseCounterReturn;UseCounterOpts
| Name | Type | Default | Description |
|---|---|---|---|
initialCount | number | 0 | The starting value of the counter. |
step | number | 1 | The amount to change the count by. |
min | number | undefined | Minimum value limit. |
max | number | undefined | Maximum value limit. |
UseCounterReturn
| Name | Type | Description |
|---|---|---|
count | number | The current value of the counter. |
increment | () => void | Increases the count by the step value. |
decrement | () => void | Decreases the count by the step value. |
reset | () => void | Resets the count to the initial value. |
setCount | (value: number) => void | Sets the counter to a specific value. |
incrementBy | (value: number) => void | Increases the count by the specified value. |
decrementBy | (value: number) => void | Decreases the count by the specified value. |
canIncrement | boolean | Whether the counter can be incremented (not at max). |
canDecrement | boolean | Whether the counter can be decremented (not at min). |
Hook Source Code
import { useState, useCallback } from "react";
/**
* Options for the useCounter hook
*/
export interface UseCounterOpts {
/** Default step value for increment/decrement (default: 1) */
step?: number;
/** Minimum value limit */
min?: number;
/** Maximum value limit */
max?: number;
}
/**
* Return type for the useCounter hook
*/
export interface UseCounterReturn {
/** Current counter value */
count: number;
/** Reset counter to initial value */
reset: () => void;
/** Increment counter by step */
increment: () => void;
/** Decrement counter by step */
decrement: () => void;
/** Set counter to a specific value */
setCount: (value: number) => void;
/** Increment counter by specific value */
incrementBy: (value: number) => void;
/** Decrement counter by specific value */
decrementBy: (value: number) => void;
/** Whether count can be incremented (not at max) */
canIncrement: boolean;
/** Whether count can be decremented (not at min) */
canDecrement: boolean;
}
/**
* A React hook for managing a counter state with increment, decrement, and reset capabilities.
*
* @param initialCount - The initial value of the counter (default: 0)
* @param options - Options object for step, min, and max limits
* @returns UseCounterReturn object with count and control methods
*/
export const useCounter = (
initialCount: number = 0,
options: UseCounterOpts = {},
): UseCounterReturn => {
const { step = 1, min, max } = options;
const [count, setInternalCount] = useState(() => {
let val = initialCount;
if (min !== undefined) val = Math.max(val, min);
if (max !== undefined) val = Math.min(val, max);
return val;
});
const increment = useCallback(() => {
setInternalCount((c) => {
const next = c + step;
if (max !== undefined && next > max) return max;
if (min !== undefined && next < min) return min;
return next;
});
}, [step, max, min]);
const decrement = useCallback(() => {
setInternalCount((c) => {
const next = c - step;
if (min !== undefined && next < min) return min;
if (max !== undefined && next > max) return max;
return next;
});
}, [step, min, max]);
const setCount = useCallback(
(val: number) => {
setInternalCount(() => {
let next = val;
if (min !== undefined) next = Math.max(next, min);
if (max !== undefined) next = Math.min(next, max);
return next;
});
},
[min, max],
);
const incrementBy = useCallback(
(val: number) => {
setInternalCount((c) => {
const next = c + val;
if (max !== undefined && next > max) return max;
if (min !== undefined && next < min) return min;
return next;
});
},
[max, min],
);
const decrementBy = useCallback(
(val: number) => {
setInternalCount((c) => {
const next = c - val;
if (min !== undefined && next < min) return min;
if (max !== undefined && next > max) return max;
return next;
});
},
[min, max],
);
const reset = useCallback(() => {
let val = initialCount;
if (min !== undefined) val = Math.max(val, min);
if (max !== undefined) val = Math.min(val, max);
setInternalCount(val);
}, [initialCount, min, max]);
return {
count,
setCount,
reset,
increment,
decrement,
incrementBy,
decrementBy,
canIncrement: max === undefined || count < max,
canDecrement: min === undefined || count > min,
};
};