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 {
  useState,
  createContext,
  ReactNode,
  use,
  useCallback,
  useEffect,
  useId,
  useMemo,
  useRef,
} 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);
 
  const getIndex = useCallback(
    (key: string) => {
      if (keys.current.includes(key)) {
        return keys.current.indexOf(key);
      }
 
      keys.current.push(key);
 
      return keys.current.length - 1;
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [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);
  }, [seed, 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, id]);
 
  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

PropDefaultTypeDescription

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

Stepper

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.