npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

achievements-react

v0.3.0

Published

React 19+ bindings for the achievements library with hooks and factory

Readme

achievements-react

React 19+ bindings for achievements. One factory call, fully typed hooks, zero boilerplate.

npm bundle size License: MIT TypeScript

npm install achievements-react
# pnpm add achievements-react
# yarn add achievements-react
# bun add achievements-react

Peer requirements: React >= 19.0.0. The achievements core is bundled. No separate install needed.

How it works

Call createAchievements() once in a module-level file. It returns a pre-wired Provider, an engine, and hooks that are already bound to your ID type. No generic annotations needed at the call site.

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

const definitions = defineAchievements([
  { id: "first-visit", label: "First Visit", description: "Open the app." },
  { id: "click-frenzy", label: "Click Frenzy", description: "Click 50 times.", maxProgress: 50 },
  { id: "night-owl", label: "Night Owl", description: "Use the app after midnight.", hidden: true },
]);

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

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

createRoot(document.getElementById("root")!).render(
  <Provider>
    <App />
  </Provider>,
);
// src/components/Example.tsx: use anywhere, fully typed
import { useAchievements, useIsUnlocked, useProgress } from "./achievements";

function Example() {
  const { unlock, incrementProgress } = useAchievements();
  const visited = useIsUnlocked("first-visit"); // boolean
  const { progress, max } = useProgress("click-frenzy"); // { progress: number, max: number }

  return (
    <>
      <button onClick={() => unlock("first-visit")}>Visit</button>
      <button onClick={() => incrementProgress("click-frenzy")}>
        Click ({progress}/{max})
      </button>
    </>
  );
}

Table of contents

createAchievements config

| Option | Type | Default | Description | | ------------------ | ------------------------------------ | ----------------------- | -------------------------------------------------- | | definitions | ReadonlyArray<AchievementDef<TId>> | - | Achievement definitions. | | storage | StorageAdapter | localStorageAdapter() | Storage backend. | | hash | HashAdapter | fnv1aHashAdapter() | Hash function for tamper detection. | | onUnlock | (id: TId) => void | - | Called synchronously when an achievement unlocks. | | onTamperDetected | (key: string) => void | - | Called when stored data fails its integrity check. |

Provider

The Provider returned by createAchievements is pre-wired to the engine, no props needed.

import { Provider } from "./achievements";

export default function RootLayout({ children }) {
  return <Provider>{children}</Provider>;
}

If you need direct control over the engine lifecycle, AchievementsProvider accepts an explicit engine prop, see Without the factory.

Hooks

All hooks must be called inside Provider. Each one subscribes only to the slice of state it needs, so a progress change on achievement A won't re-render a component that only watches achievement B.

useAchievements

Returns the engine for imperative calls. The source of truth for mutations.

import { useAchievements } from "./achievements";

function Controls() {
  const { unlock, incrementProgress, collectItem, reset } = useAchievements();

  return <button onClick={() => unlock("first-visit")}>Trigger</button>;
}

All engine methods are available: unlock, setProgress, incrementProgress, collectItem, setMaxProgress, dismissToast, reset, isUnlocked, getProgress, getItems, getUnlocked, getUnlockedCount, getState, getDefinition, subscribe.

useIsUnlocked

Reactive boolean. Re-renders only when this achievement's lock state changes.

import { useIsUnlocked } from "./achievements";

function Badge() {
  const unlocked = useIsUnlocked("first-visit");
  return <span className={unlocked ? "gold" : "grey"}>★</span>;
}

useProgress

Reactive { progress, max }. Re-renders only when this achievement's progress changes.

import { useProgress } from "./achievements";

function ProgressBar() {
  const { progress, max } = useProgress("click-frenzy");
  // max is undefined for achievements without maxProgress

  return <progress value={progress} max={max} />;
}

| Field | Type | Description | | ---------- | --------------------- | -------------------------------------------------- | | progress | number | Current progress (0 if unset). | | max | number \| undefined | maxProgress from the definition, or undefined. |

useUnlockedCount

Reactive count of unlocked achievements. Re-renders only when the total changes.

import { useUnlockedCount } from "./achievements";

function Score() {
  const count = useUnlockedCount();
  return (
    <p>
      {count} / {definitions.length} unlocked
    </p>
  );
}

useAchievementToast

Returns the toast queue (oldest-first) and a dismiss function. Display queue[0], then call dismiss(queue[0]) once the notification closes.

import { useEffect } from "react";
import { useAchievementToast, engine } from "./achievements";

const DISPLAY_MS = 3000;

function Toast() {
  const { queue, dismiss } = useAchievementToast();
  const id = queue[0];
  const def = id ? engine.getDefinition(id) : undefined;

  useEffect(() => {
    if (!id) return;
    const t = setTimeout(() => dismiss(id), DISPLAY_MS);
    return () => clearTimeout(t);
  }, [id, dismiss]);

  if (!id || !def) return null;

  return (
    <div role="status" className="toast">
      <strong>{def.label}</strong>
      <p>{def.description}</p>
      <button onClick={() => dismiss(id)}>x</button>
    </div>
  );
}

| Field | Type | Description | | --------- | -------------------- | ------------------------------------------------ | | queue | ReadonlyArray<TId> | IDs waiting to be shown, oldest first. | | dismiss | (id: TId) => void | Remove an ID from the queue after displaying it. |

useTamperDetected

Returns the storage key that failed its integrity check, or null if clean. Handles detection both at module-load time (before React mounts) and at runtime.

import { useTamperDetected, Provider } from "./achievements";

export default function App() {
  // Must be called OUTSIDE Provider: it catches tamper events
  // that fire during engine init, before any component mounts.
  const tamperKey = useTamperDetected();

  if (tamperKey !== null) {
    return (
      <div>
        <h1>Cheating detected</h1>
        <p>
          Modified key: <code>{tamperKey}</code>
        </p>
        <button onClick={() => window.location.reload()}>Reload</button>
      </div>
    );
  }

  return (
    <Provider>
      <Main />
    </Provider>
  );
}

Using the engine directly

The engine export is the raw core engine. You can call it anywhere: inside event listeners, WebSocket handlers, or plain utility functions. No hooks required.

import { engine } from "./achievements";

// Respond to external events
socket.on("level-complete", () => engine.unlock("level-complete"));

// Read synchronously, outside React
const alreadyVisited = engine.isUnlocked("first-visit");

Without the factory

If you prefer to manage the engine lifecycle yourself, use AchievementsProvider and the unbound hooks directly. You'll need to pass the type parameter manually at each call site.

import { createAchievements } from "achievements";
import { AchievementsProvider, useAchievements, useIsUnlocked } from "achievements-react";

const engine = createAchievements<AchievementId>({ definitions });

function Root() {
  return (
    <AchievementsProvider engine={engine}>
      <App />
    </AchievementsProvider>
  );
}

// In components: note the explicit <AchievementId>
const { unlock } = useAchievements<AchievementId>();
const unlocked = useIsUnlocked<AchievementId>("first-visit");

Re-exports from core

achievements-react re-exports everything from achievements, so you rarely need to import from two packages:

import {
  // Factory & components
  createAchievements,
  AchievementsProvider,

  // Hooks
  useAchievements,
  useIsUnlocked,
  useProgress,
  useAchievementToast,
  useUnlockedCount,
  useTamperDetected,

  // Re-exported from core
  defineAchievements,
  localStorageAdapter,
  inMemoryAdapter,
  fnv1aHashAdapter,

  // Types
  type AchievementDef,
  type AchievementState,
  type AchievementEngine,
  type StorageAdapter,
  type HashAdapter,
  type AchievementsConfig,
} from "achievements-react";