Skip to main content
Factory that creates the achievement engine and returns hooks already bound to your achievement ID type. This eliminates the need to write useHook<AchievementId>() everywhere - all hooks are pre-typed.

Signature

function createAchievements<TId extends string>(
  config: AchievementsConfig<TId>
): {
  engine: AchievementEngine<TId>;
  Provider: ({ children }: { children: ReactNode }) => JSX.Element;
  useAchievements: () => AchievementEngine<TId>;
  useIsUnlocked: (id: TId) => boolean;
  useProgress: (id: TId) => { progress: number; max: number | undefined };
  useAchievementToast: () => { queue: ReadonlyArray<TId>; dismiss: (id: TId) => void };
  useUnlockedCount: () => number;
  useTamperDetected: () => string | null;
}

Parameters

config
AchievementsConfig<TId>
required
Configuration object for the achievement system.

Returns

engine
AchievementEngine<TId>
The core achievement engine instance for imperative API calls.
Provider
({ children }: { children: ReactNode }) => JSX.Element
Drop-in React context provider component with the engine already bound - no engine prop needed.
useAchievements
() => AchievementEngine<TId>
Hook that returns the engine for imperative calls (unlock, setProgress, etc.).
useIsUnlocked
(id: TId) => boolean
Reactive hook that returns whether a specific achievement is unlocked. Re-renders only when that achievement’s lock state changes.
useProgress
(id: TId) => { progress: number; max: number | undefined }
Reactive hook that returns the current progress and max progress for an achievement. Re-renders only when that achievement’s progress changes.
useAchievementToast
() => { queue: ReadonlyArray<TId>; dismiss: (id: TId) => void }
Reactive hook for toast notifications with queue and dismiss helper.
useUnlockedCount
() => number
Reactive hook that returns the total count of unlocked achievements. Re-renders only when the count changes.
useTamperDetected
() => string | null
Returns the storage key that failed its integrity check, or null if none. Handles both pre-mount and runtime tamper detection.

Example

// achievements.ts
import {
  defineAchievements,
  createAchievements,
  localStorageAdapter,
} from "achievements-react";

export const definitions = defineAchievements([
  {
    id: "first-visit",
    label: "First Contact",
    description: "Initialized session for the first time.",
  },
  {
    id: "click-frenzy",
    label: "Input Overflow",
    description: "Registered 50 consecutive inputs.",
    maxProgress: 50,
  },
]);

export type AchievementId = (typeof definitions)[number]["id"];

export const {
  engine,
  Provider,
  useAchievements,
  useIsUnlocked,
  useProgress,
  useAchievementToast,
  useUnlockedCount,
  useTamperDetected,
} = createAchievements<AchievementId>({
  definitions,
  storage: localStorageAdapter("my-app"),
});
// App.tsx
import { Provider, useTamperDetected } from "./achievements";
import { Dashboard } from "./Dashboard";

export default function App() {
  const tamperKey = useTamperDetected();

  if (tamperKey !== null) {
    return <div>Tamper detected on key: {tamperKey}</div>;
  }

  return (
    <Provider>
      <Dashboard />
    </Provider>
  );
}
// Component.tsx
import { useIsUnlocked } from "./achievements";

function MyComponent() {
  // Fully typed - no <T> needed!
  const unlocked = useIsUnlocked("click-frenzy");
  
  return <div>{unlocked ? "Unlocked!" : "Locked"}</div>;
}

Notes

  • All returned hooks are already bound to your TId type, eliminating the need for generic type parameters in components
  • The Provider component has the engine pre-bound, so you don’t need to pass an engine prop
  • The factory handles tamper detection buffering, ensuring useTamperDetected() works even when tampering is detected before React mounts
  • You can still access the engine directly for imperative operations or use it outside of React components

Build docs developers (and LLMs) love