Skip to main content

Custom Storage

Create your own storage implementation for React Achievements.

Storage Interface

To create a custom storage backend, implement either the synchronous AchievementStorage interface or the asynchronous AsyncAchievementStorage interface.

Synchronous Storage Interface

interface AchievementStorage {
getMetrics(): AchievementMetrics;
setMetrics(metrics: AchievementMetrics): void;
getUnlockedAchievements(): string[];
setUnlockedAchievements(achievements: string[]): void;
clear(): void;
}

Asynchronous Storage Interface

interface AsyncAchievementStorage {
getMetrics(): Promise<AchievementMetrics>;
setMetrics(metrics: AchievementMetrics): Promise<void>;
getUnlockedAchievements(): Promise<string[]>;
setUnlockedAchievements(achievements: string[]): Promise<void>;
clear(): Promise<void>;
}

Example: Custom SessionStorage Implementation

import { AchievementStorage, AchievementMetrics } from 'react-achievements';

export class SessionStorageAdapter implements AchievementStorage {
private keyPrefix: string;

constructor(keyPrefix = 'achievements') {
this.keyPrefix = keyPrefix;
}

private getKey(key: string): string {
return `${this.keyPrefix}_${key}`;
}

getMetrics(): AchievementMetrics {
const data = sessionStorage.getItem(this.getKey('metrics'));
return data ? JSON.parse(data) : {};
}

setMetrics(metrics: AchievementMetrics): void {
sessionStorage.setItem(
this.getKey('metrics'),
JSON.stringify(metrics)
);
}

getUnlockedAchievements(): string[] {
const data = sessionStorage.getItem(this.getKey('unlocked'));
return data ? JSON.parse(data) : [];
}

setUnlockedAchievements(achievements: string[]): void {
sessionStorage.setItem(
this.getKey('unlocked'),
JSON.stringify(achievements)
);
}

clear(): void {
sessionStorage.removeItem(this.getKey('metrics'));
sessionStorage.removeItem(this.getKey('unlocked'));
}
}

Usage

import { AchievementProvider } from 'react-achievements';
import { SessionStorageAdapter } from './SessionStorageAdapter';

const sessionStorage = new SessionStorageAdapter();

function App() {
return (
<AchievementProvider
achievements={gameAchievements}
storage={sessionStorage}
>
{/* Your app */}
</AchievementProvider>
);
}

Example: Firebase Storage Implementation

import { AsyncAchievementStorage, AchievementMetrics } from 'react-achievements';
import { getFirestore, doc, setDoc, getDoc } from 'firebase/firestore';

export class FirebaseStorage implements AsyncAchievementStorage {
private db;
private userId: string;

constructor(userId: string) {
this.db = getFirestore();
this.userId = userId;
}

private get docRef() {
return doc(this.db, 'achievements', this.userId);
}

async getMetrics(): Promise<AchievementMetrics> {
const snapshot = await getDoc(this.docRef);
return snapshot.data()?.metrics || {};
}

async setMetrics(metrics: AchievementMetrics): Promise<void> {
await setDoc(this.docRef, { metrics }, { merge: true });
}

async getUnlockedAchievements(): Promise<string[]> {
const snapshot = await getDoc(this.docRef);
return snapshot.data()?.unlocked || [];
}

async setUnlockedAchievements(achievements: string[]): Promise<void> {
await setDoc(this.docRef, { unlocked: achievements }, { merge: true });
}

async clear(): Promise<void> {
await setDoc(this.docRef, { metrics: {}, unlocked: [] });
}
}

Usage with Async Storage

Async storage is automatically wrapped with the AsyncStorageAdapter:

import { AchievementProvider } from 'react-achievements';
import { FirebaseStorage } from './FirebaseStorage';

const firebaseStorage = new FirebaseStorage('user123');

function App() {
return (
<AchievementProvider
achievements={gameAchievements}
storage={firebaseStorage}
>
{/* Your app */}
</AchievementProvider>
);
}

The provider automatically detects async storage and wraps it with optimistic updates.

Example: Supabase Storage

import { AsyncAchievementStorage, AchievementMetrics } from 'react-achievements';
import { createClient, SupabaseClient } from '@supabase/supabase-js';

export class SupabaseStorage implements AsyncAchievementStorage {
private supabase: SupabaseClient;
private userId: string;

constructor(supabaseUrl: string, supabaseKey: string, userId: string) {
this.supabase = createClient(supabaseUrl, supabaseKey);
this.userId = userId;
}

async getMetrics(): Promise<AchievementMetrics> {
const { data, error } = await this.supabase
.from('achievements')
.select('metrics')
.eq('user_id', this.userId)
.single();

if (error || !data) return {};
return data.metrics;
}

async setMetrics(metrics: AchievementMetrics): Promise<void> {
await this.supabase
.from('achievements')
.upsert({
user_id: this.userId,
metrics,
updated_at: new Date().toISOString(),
});
}

async getUnlockedAchievements(): Promise<string[]> {
const { data, error } = await this.supabase
.from('achievements')
.select('unlocked')
.eq('user_id', this.userId)
.single();

if (error || !data) return [];
return data.unlocked;
}

async setUnlockedAchievements(achievements: string[]): Promise<void> {
await this.supabase
.from('achievements')
.upsert({
user_id: this.userId,
unlocked: achievements,
updated_at: new Date().toISOString(),
});
}

async clear(): Promise<void> {
await this.supabase
.from('achievements')
.delete()
.eq('user_id', this.userId);
}
}

Best Practices

1. Handle Errors Gracefully

async getMetrics(): Promise<AchievementMetrics> {
try {
const response = await fetch(`${this.apiUrl}/metrics`);
if (!response.ok) {
console.error('Failed to fetch metrics');
return {}; // Return empty metrics on error
}
return await response.json();
} catch (error) {
console.error('Error fetching metrics:', error);
return {};
}
}

2. Implement Caching for Async Storage

export class CachedStorage implements AsyncAchievementStorage {
private cache: { metrics?: AchievementMetrics; unlocked?: string[] } = {};
private underlyingStorage: AsyncAchievementStorage;

constructor(storage: AsyncAchievementStorage) {
this.underlyingStorage = storage;
}

async getMetrics(): Promise<AchievementMetrics> {
if (!this.cache.metrics) {
this.cache.metrics = await this.underlyingStorage.getMetrics();
}
return this.cache.metrics;
}

async setMetrics(metrics: AchievementMetrics): Promise<void> {
this.cache.metrics = metrics;
await this.underlyingStorage.setMetrics(metrics);
}

// ... implement other methods similarly
}

3. Add Migration Support

export class MigratableStorage implements AchievementStorage {
private version = 2;
private keyPrefix: string;

constructor(keyPrefix = 'achievements') {
this.keyPrefix = keyPrefix;
this.migrateIfNeeded();
}

private migrateIfNeeded(): void {
const currentVersion = localStorage.getItem(`${this.keyPrefix}_version`);

if (!currentVersion || parseInt(currentVersion) < this.version) {
this.migrate(parseInt(currentVersion || '0'));
localStorage.setItem(`${this.keyPrefix}_version`, String(this.version));
}
}

private migrate(fromVersion: number): void {
if (fromVersion < 2) {
// Migrate data from version 1 to version 2
// ... migration logic
}
}

// ... implement storage interface
}

Testing Custom Storage

import { render } from '@testing-library/react';
import { AchievementProvider } from 'react-achievements';
import { MyCustomStorage } from './MyCustomStorage';

describe('MyCustomStorage', () => {
it('should store and retrieve metrics', () => {
const storage = new MyCustomStorage();
const metrics = { score: [100] };

storage.setMetrics(metrics);
const retrieved = storage.getMetrics();

expect(retrieved).toEqual(metrics);
});

it('should work with AchievementProvider', () => {
const storage = new MyCustomStorage();

render(
<AchievementProvider
achievements={testAchievements}
storage={storage}
>
<TestComponent />
</AchievementProvider>
);

// ... test your component
});
});