This guide will help you set up achievements-manager in your project. Choose the section that matches your framework:
React React 19+ with hooks and factory
Vanilla JS Framework-agnostic core engine
React
For React applications, use the factory pattern to create typed hooks and a Provider in one call.
Install the package
npm install achievements-react
Define your achievements
Create a module-level file that exports your typed hooks and engine: 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 },
])
export type AchievementId = typeof definitions [ number ][ 'id' ]
export const { engine , Provider , useAchievements , useIsUnlocked , useProgress } =
createAchievements < AchievementId >({
definitions ,
storage: localStorageAdapter ( 'my-app' ),
})
The defineAchievements helper provides full type inference for your achievement IDs. No manual type annotations needed!
Wrap your app with the Provider
Add the Provider to your root component: import { Provider } from './achievements'
import { createRoot } from 'react-dom/client'
import App from './App'
createRoot ( document . getElementById ( 'root' ) ! ). render (
< Provider >
< App />
</ Provider > ,
)
Use the hooks in your components
Import and use the pre-typed hooks anywhere inside the Provider: src/components/Example.tsx
import { useAchievements , useIsUnlocked , useProgress } from '../achievements'
function Example () {
const { unlock , incrementProgress } = useAchievements ()
const visited = useIsUnlocked ( 'first-visit' )
const { progress , max } = useProgress ( 'click-frenzy' )
return (
<>
< button onClick = { () => unlock ( 'first-visit' ) } > Visit </ button >
< button onClick = { () => incrementProgress ( 'click-frenzy' ) } >
Click ( { progress } / { max } )
</ button >
{ visited && < p > Welcome back! </ p > }
</>
)
}
All hooks are fully typed with your custom AchievementId type. No generic annotations needed at the call site!
Available React Hooks
The factory returns these pre-typed hooks:
Hook Returns Description useAchievementsAchievementEngine<TId>Full engine with all methods useIsUnlocked(id)booleanRe-renders only when this achievement’s lock state changes useProgress(id){ progress: number, max?: number }Re-renders only when this achievement’s progress changes useUnlockedCountnumberRe-renders only when the total count changes useAchievementToast{ queue, dismiss }Toast queue for displaying notifications useTamperDetectedstring | nullStorage key that failed integrity check
Vanilla / Framework-Agnostic
For vanilla JavaScript, Vue, Svelte, or server-side use, work directly with the engine:
Create the engine
import { createAchievements , localStorageAdapter } from 'achievements'
const engine = createAchievements ({
definitions: [
{ id: 'first-visit' , label: 'First Visit' , description: 'Open the app.' },
],
storage: localStorageAdapter ( 'my-app' ),
onUnlock : ( id ) => showNotification ( id ),
})
engine . unlock ( 'first-visit' )
engine . subscribe (( state ) => render ( state ))
Track achievements
Call engine methods from anywhere in your app: // Unlock achievements
engine . unlock ( 'first-visit' )
// Track progress
engine . setProgress ( 'click-frenzy' , 25 )
engine . incrementProgress ( 'click-frenzy' )
// Collect items (idempotent)
engine . collectItem ( 'explorer' , 'module-core' )
engine . collectItem ( 'explorer' , 'module-react' )
// Read state
const unlocked = engine . isUnlocked ( 'first-visit' )
const progress = engine . getProgress ( 'click-frenzy' )
const count = engine . getUnlockedCount ()
Subscribe to changes
Listen for state updates and render your UI: engine . subscribe (( state ) => {
console . log ( 'Unlocked:' , [ ... state . unlockedIds ])
console . log ( 'Progress:' , state . progress )
console . log ( 'Toast queue:' , state . toastQueue )
// Update your UI
renderAchievements ( state )
})
The subscribe callback receives a full state snapshot after every mutation. Use it to keep your UI in sync.
Engine API Methods
Write Methods
unlock(id) - Unlocks an achievement
setProgress(id, value) - Sets absolute progress value
incrementProgress(id) - Increments progress by 1
collectItem(id, item) - Adds unique item to set
setMaxProgress(id, max) - Updates max progress at runtime
dismissToast(id) - Removes ID from toast queue
reset() - Wipes all state and storage
Read Methods
isUnlocked(id) - Returns boolean
getProgress(id) - Returns current progress number
getItems(id) - Returns Set of collected items
getUnlocked() - Returns Set of all unlocked IDs
getUnlockedCount() - Returns number of unlocked
getDefinition(id) - Returns achievement definition
getState() - Returns full state snapshot
Achievement Definition Fields
When defining achievements, use these fields:
Field Type Required Description idstringYes Unique identifier (inferred as literal type) labelstringYes Human-readable name for display descriptionstringYes Short description of unlock condition maxProgressnumberNo Enables progress tracking, auto-unlocks at threshold hiddenbooleanNo Hides achievement entirely until unlocked hintbooleanNo Hides only description until unlocked
Storage Adapters
The library includes built-in storage adapters:
localStorage
In-Memory
Custom
import { localStorageAdapter } from 'achievements'
// With prefix (recommended)
const storage = localStorageAdapter ( 'my-app' )
// Keys: "my-app:unlocked", "my-app:progress", etc.
// Without prefix
const storage = localStorageAdapter ()
Anti-Cheat & Tamper Detection
Every persisted entry is stored with an integrity hash (FNV-1a by default). On load, the hash is recomputed:
const engine = createAchievements ({
definitions ,
storage: localStorageAdapter ( 'my-app' ),
onTamperDetected : ( key ) => {
console . warn ( 'Tamper detected on key:' , key )
// The corrupted entry is automatically wiped
// You can show a warning or reset progress
},
})
Hashes are stored in localStorage as plain strings, so a determined user can still forge them. This is a friction layer, not cryptographic security.
Complete Example
Here’s a complete working example with progress tracking and toasts:
import { createAchievements , defineAchievements , localStorageAdapter } from 'achievements-react'
import { useEffect } from 'react'
// Define achievements
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 ,
} = createAchievements < AchievementId >({
definitions ,
storage: localStorageAdapter ( 'demo-app' ),
onUnlock : ( id ) => console . log ( '🎉 Unlocked:' , id ),
})
// Toast component
function AchievementToast () {
const { queue , dismiss } = useAchievementToast ()
const id = queue [ 0 ]
const def = id ? engine . getDefinition ( id ) : undefined
useEffect (() => {
if ( ! id ) return
const timer = setTimeout (() => dismiss ( id ), 3000 )
return () => clearTimeout ( timer )
}, [ id , dismiss ])
if ( ! id || ! def ) return null
return (
< div className = "toast" >
< strong > { def . label } </ strong >
< p > { def . description } </ p >
< button onClick = { () => dismiss ( id ) } > × </ button >
</ div >
)
}
// Main component
function App () {
const { unlock , incrementProgress } = useAchievements ()
const { progress , max } = useProgress ( 'click-frenzy' )
const count = useUnlockedCount ()
return (
< Provider >
< AchievementToast />
< h1 > Achievements Demo </ h1 >
< p > Unlocked: { count } / { definitions . length } </ p >
< button onClick = { () => unlock ( 'first-visit' ) } >
Unlock First Visit
</ button >
< button onClick = { () => incrementProgress ( 'click-frenzy' ) } >
Click Me ( { progress } / { max } )
</ button >
</ Provider >
)
}
Next Steps
Core API Reference Complete engine API documentation
React Hooks Detailed guide to all React hooks
Storage Adapters Learn about storage backends and custom adapters
Anti-cheat Tamper detection and hash adapters