TourKit
@tour-kit/checklistsHooks

useChecklistPersistence

useChecklistPersistence hook: save and restore checklist completion state across sessions with pluggable storage adapters

useChecklistPersistence

Hook for managing checklist state persistence. Typically used internally by ChecklistProvider, but can be used directly for custom persistence strategies.

Usage

import { useChecklistPersistence } from '@tour-kit/checklists';

function CustomProvider() {
  const { save, load, clear } = useChecklistPersistence({
    enabled: true,
    storage: 'localStorage',
    key: 'my-checklists',
  });

  // Load on mount
  useEffect(() => {
    const state = load();
    if (state) {
      // Apply loaded state
      applyState(state);
    }
  }, [load]);

  // Save on change
  useEffect(() => {
    save(currentState);
  }, [currentState, save]);

  return <div>{/* Your UI */}</div>;
}

The ChecklistProvider handles persistence automatically. You only need this hook for custom implementations.

Parameters

Prop

Type

Config Object

Prop

Type

Return Value

Prop

Type

Persisted State Format

interface PersistedChecklistState {
  // Completed tasks per checklist
  completed: Record<string, string[]>;

  // Dismissed checklist IDs
  dismissed: string[];

  // Timestamp of last update
  timestamp: number;
}

Example:

{
  "completed": {
    "onboarding": ["step1", "step2"],
    "setup": ["verify-email", "add-payment"]
  },
  "dismissed": ["old-checklist"],
  "timestamp": 1234567890
}

Storage Types

localStorage

Persists across browser sessions:

const persistence = useChecklistPersistence({
  enabled: true,
  storage: 'localStorage',
  key: 'my-checklists',
});

sessionStorage

Persists only during browser session:

const persistence = useChecklistPersistence({
  enabled: true,
  storage: 'sessionStorage',
  key: 'my-checklists',
});

Memory

In-memory storage (useful for testing or SSR):

const persistence = useChecklistPersistence({
  enabled: true,
  storage: 'memory',
});

Memory storage is cleared when the component unmounts or page reloads.

Custom Storage

Database Storage

function DatabasePersistence() {
  const persistence = useChecklistPersistence({
    enabled: true,
    onSave: async (state) => {
      await api.saveChecklistState(userId, state);
    },
    onLoad: async () => {
      const data = await api.loadChecklistState(userId);
      return data;
    },
  });

  return <div>{/* Your UI */}</div>;
}

When using async onLoad, it returns null initially. Handle loading state in your component.

IndexedDB Storage

import { openDB } from 'idb';

const db = await openDB('checklists', 1, {
  upgrade(db) {
    db.createObjectStore('state');
  },
});

function IndexedDBPersistence() {
  const persistence = useChecklistPersistence({
    enabled: true,
    onSave: async (state) => {
      await db.put('state', state, 'current');
    },
    onLoad: async () => {
      return (await db.get('state', 'current')) ?? null;
    },
  });

  return <div>{/* Your UI */}</div>;
}

URL State

function URLPersistence() {
  const [searchParams, setSearchParams] = useSearchParams();

  const persistence = useChecklistPersistence({
    enabled: true,
    onSave: (state) => {
      const encoded = btoa(JSON.stringify(state));
      setSearchParams({ checklist: encoded });
    },
    onLoad: () => {
      const encoded = searchParams.get('checklist');
      if (!encoded) return null;
      try {
        return JSON.parse(atob(encoded));
      } catch {
        return null;
      }
    },
  });

  return <div>{/* Your UI */}</div>;
}

Error Handling

The hook handles errors gracefully:

const persistence = useChecklistPersistence({
  enabled: true,
  storage: 'localStorage',
});

// Save errors are logged but don't throw
persistence.save(state); // Logs warning if quota exceeded

// Load errors return null
const state = persistence.load(); // Returns null if parse error

Migration Strategies

Version Migration

interface PersistedStateV1 {
  completed: Record<string, string[]>;
}

interface PersistedStateV2 extends PersistedStateV1 {
  dismissed: string[];
  timestamp: number;
}

function MigrationPersistence() {
  const persistence = useChecklistPersistence({
    enabled: true,
    onLoad: () => {
      const raw = localStorage.getItem('my-checklists');
      if (!raw) return null;

      const data = JSON.parse(raw);

      // Migrate v1 to v2
      if (!data.timestamp) {
        return {
          completed: data.completed ?? {},
          dismissed: [],
          timestamp: Date.now(),
        };
      }

      return data as PersistedStateV2;
    },
  });

  return <div>{/* Your UI */}</div>;
}

Data Cleanup

function CleanupPersistence() {
  const persistence = useChecklistPersistence({
    enabled: true,
    onSave: (state) => {
      // Remove old data (older than 30 days)
      const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;

      if (state.timestamp < thirtyDaysAgo) {
        persistence.clear();
        return;
      }

      // Save normally
      localStorage.setItem('my-checklists', JSON.stringify(state));
    },
  });

  return <div>{/* Your UI */}</div>;
}

SSR Considerations

The hook is SSR-safe:

// On server: uses memory storage
// On client: uses specified storage

function SSRSafeApp() {
  const persistence = useChecklistPersistence({
    enabled: true,
    storage: 'localStorage', // Falls back to memory on server
  });

  // Safe to use
  return <div>{/* Your UI */}</div>;
}

Manual Control

Clear All State

function ClearButton() {
  const { clear } = useChecklistPersistence({
    enabled: true,
  });

  return (
    <button onClick={clear}>
      Clear all progress
    </button>
  );
}

Export State

function ExportButton() {
  const { load } = useChecklistPersistence({
    enabled: true,
  });

  const handleExport = () => {
    const state = load();
    if (!state) return;

    const json = JSON.stringify(state, null, 2);
    const blob = new Blob([json], { type: 'application/json' });
    const url = URL.createObjectURL(blob);

    const a = document.createElement('a');
    a.href = url;
    a.download = 'checklist-progress.json';
    a.click();

    URL.revokeObjectURL(url);
  };

  return (
    <button onClick={handleExport}>
      Export progress
    </button>
  );
}

Import State

function ImportButton() {
  const { save } = useChecklistPersistence({
    enabled: true,
  });

  const handleImport = (event: React.ChangeEvent<HTMLInputElement>) => {
    const file = event.target.files?.[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onload = (e) => {
      try {
        const state = JSON.parse(e.target?.result as string);
        save(state);
        window.location.reload();
      } catch (error) {
        console.error('Invalid file:', error);
      }
    };
    reader.readAsText(file);
  };

  return (
    <input
      type="file"
      accept=".json"
      onChange={handleImport}
    />
  );
}

Best Practices

Debounce Saves

import { useDebouncedCallback } from 'use-debounce';

function DebouncedPersistence() {
  const { save } = useChecklistPersistence({
    enabled: true,
  });

  const debouncedSave = useDebouncedCallback(
    (state) => save(state),
    1000 // Save at most once per second
  );

  useEffect(() => {
    debouncedSave(currentState);
  }, [currentState, debouncedSave]);
}

Validate Loaded State

function ValidatedPersistence() {
  const persistence = useChecklistPersistence({
    enabled: true,
    onLoad: () => {
      const state = localStorage.getItem('my-checklists');
      if (!state) return null;

      try {
        const parsed = JSON.parse(state);

        // Validate structure
        if (
          typeof parsed !== 'object' ||
          !parsed.completed ||
          !parsed.dismissed ||
          !parsed.timestamp
        ) {
          console.warn('Invalid state structure');
          return null;
        }

        return parsed;
      } catch {
        return null;
      }
    },
  });
}

Handle Storage Quota

function QuotaSafePersistence() {
  const persistence = useChecklistPersistence({
    enabled: true,
    onSave: (state) => {
      try {
        localStorage.setItem('my-checklists', JSON.stringify(state));
      } catch (error) {
        if (error.name === 'QuotaExceededError') {
          // Clear old data or notify user
          console.warn('Storage quota exceeded, clearing old data');
          localStorage.clear();
          localStorage.setItem('my-checklists', JSON.stringify(state));
        }
      }
    },
  });
}

On this page