Instance Counter
A utility component that assigns unique indices to component instances and tracks their total count in the component tree.
Source Code
'use client';
import {
createContext,
type ReactNode,
use,
useCallback,
useEffect,
useId,
useMemo,
useRef,
useState,
} from 'react';
interface InstanceCounterContextType {
getIndex: (key: string) => number;
invalidate: () => void;
}
const InstanceCounterContext = createContext<InstanceCounterContextType>({
getIndex: () => 0,
invalidate: () => {},
});
interface InstanceCounterProviderProps {
children: ReactNode;
onChange?: (length: number) => void;
}
const InstanceCounterProvider = ({
children,
onChange,
}: InstanceCounterProviderProps) => {
const [seed, setSeed] = useState(0);
const keys = useRef<string[]>([]);
const isMounted = useRef(false);
// biome-ignore lint/correctness/useExhaustiveDependencies: intentional
const getIndex = useCallback(
(key: string) => {
if (keys.current.includes(key)) {
return keys.current.indexOf(key);
}
keys.current.push(key);
return keys.current.length - 1;
},
[seed]
);
const invalidate = useCallback(() => {
if (isMounted.current) {
keys.current = [];
setSeed((prev) => prev + 1);
}
}, []);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
useEffect(() => {
onChange?.(keys.current.length);
}, [onChange]);
return (
<InstanceCounterContext value={{ getIndex, invalidate }}>
{children}
</InstanceCounterContext>
);
};
const useInstanceCounter = () => {
const id = useId();
const context = use(InstanceCounterContext);
if (!context) {
throw new Error(
'useInstanceCounter must be used within an InstanceCounterProvider'
);
}
const { getIndex, invalidate } = context;
useEffect(() => {
invalidate();
return () => invalidate();
}, [invalidate]);
return useMemo(() => getIndex(id), [getIndex, id]);
};
export { InstanceCounterProvider, useInstanceCounter }; Features
-
Automatic Index Assignment: Automatically assigns unique indices to component instances within a provider’s scope.
-
Tree-Aware: Maintains consistent indices across the entire component tree, even with deeply nested components.
-
Mount/Unmount Handling: Automatically invalidates and recalculates indices when components mount or unmount, ensuring consistent ordering.
-
Instance Count Tracking: Provides a callback to track the total number of instances in the component tree.
API Reference
InstanceCounterProvider
| Prop | Default | Type | Description |
|---|---|---|---|
onChange | - | (length: number) => void | The callback to call when the number of instances changes. |
useInstanceCounter
Returns a number that represents the current index of the component within the InstanceCounterProvider.
Examples
Basic Usage
'use client';
import { useState } from 'react';
import {
InstanceCounterProvider,
useInstanceCounter,
} from '@/components/instance-counter';
import { Button } from '@/components/button';
const Item = () => {
const index = useInstanceCounter();
return (
<div className="my-2 w-fit rounded-md bg-background-secondary px-2 py-1 text-xs">
Instance Index: {index}
</div>
);
};
const InstanceCounterPreview = () => {
const [mount, setMount] = useState(false);
const [length, setLength] = useState(0);
return (
<InstanceCounterProvider onChange={setLength}>
<div className="flex min-h-88 flex-col gap-4">
<div className="text-foreground-secondary text-sm">
Number of Instances: {length}
</div>
<Button size="sm" onClick={() => setMount(!mount)}>
Trigger Tree Change
</Button>
<div className="rounded-lg border border-border px-4 py-2 [&_*_*]:ml-4">
<Item />
<div>
<Item />
{mount && <Item />}
<div>
<div>
<Item />
<div>
<Item />
<div>
<div>
<div>
<Item />
</div>
</div>
</div>
</div>
</div>
</div>
<Item />
</div>
</div>
</InstanceCounterProvider>
);
};
export default InstanceCounterPreview; Stepper
'use client';
import { createContext, type ReactNode, use, useState } from 'react';
import {
InstanceCounterProvider,
useInstanceCounter,
} from '@/components/instance-counter';
import { Button } from '@/components/button';
const ITEMS = ['🥚', '🐣', '🐥', '🐓'];
const StepperContext = createContext(0);
const Stepper = ({ children }: { children: ReactNode }) => {
const [currentIndex, setCurrentIndex] = useState(0);
const [length, setLength] = useState(0);
return (
<InstanceCounterProvider onChange={setLength}>
<StepperContext value={currentIndex}>
<div className="flex flex-col gap-4 rounded-lg border border-border p-4">
<div className="flex gap-2">
<Button
className="grow"
size="sm"
onClick={() => setCurrentIndex((c) => (c - 1 + length) % length)}
>
←
</Button>
<Button
className="grow"
size="sm"
onClick={() => setCurrentIndex((c) => (c + 1) % length)}
>
→
</Button>
</div>
<div className="min-w-48 rounded-lg border border-border bg-background-secondary p-8 text-center">
{children}
</div>
</div>
</StepperContext>
</InstanceCounterProvider>
);
};
const StepperItem = ({ children }: { children: ReactNode }) => {
const currentIndex = use(StepperContext);
const index = useInstanceCounter();
const isActive = index === currentIndex;
return <>{isActive && children}</>;
};
const InstanceCounterStepper = () => {
return (
<Stepper>
{ITEMS.map((item, index) => (
<StepperItem key={index}>
<div className="text-[32px]">{item}</div>
</StepperItem>
))}
</Stepper>
);
};
export default InstanceCounterStepper; Limitations
The indices are assigned based on the order of React’s useEffect hook execution. While this generally matches the DOM order, if, for some reason, it does not, it may lead to unexpected behavior.
Previous
About
Next
Marquee