The useLocalProgressStore hook provides local storage persistence for episode watch states when users are not authenticated. It uses Zustand with localStorage middleware to maintain watch progress across browser sessions.
Overview
This store is automatically used by higher-level hooks like useEpisodeWatched when the user is not signed in. You typically won’t need to use this hook directly unless you’re building custom progress tracking functionality.
Basic Usage
import { useLocalProgressStore } from '@/hooks/useLocalProgressStore';
function CustomProgressTracker() {
const watchedEpisodes = useLocalProgressStore((state) => state.watchedEpisodes);
const markEpisodeWatched = useLocalProgressStore((state) => state.markEpisodeWatched);
const handleMarkWatched = () => {
// Mark Breaking Bad S1E1 as watched
markEpisodeWatched(1396, 1, 1, true);
};
// Check if episode is watched
const isWatched = watchedEpisodes['1396:1:1'] === true;
return (
<button onClick={handleMarkWatched}>
{isWatched ? 'Watched ✓' : 'Mark as Watched'}
</button>
);
}
Store State
Dictionary of watched episode states. Keys use the format ${tmdbId}:${season}:${episode}.Example:{
"1396:1:1": true, // Breaking Bad S1E1
"1396:1:2": true, // Breaking Bad S1E2
"82856:2:5": true // The Mandalorian S2E5
}
Store Actions
markEpisodeWatched
Mark a single episode as watched or unwatched.
const markEpisodeWatched = useLocalProgressStore(
(state) => state.markEpisodeWatched
);
// Mark as watched
markEpisodeWatched(1396, 1, 1, true);
// Mark as unwatched
markEpisodeWatched(1396, 1, 1, false);
Parameters
Season number (typically 1-based, but can be 0 for specials)
Episode number within the season
true to mark as watched, false to mark as unwatched
Behavior
- When
isWatched is true, adds the episode key to watchedEpisodes with value true
- When
isWatched is false, removes the episode key from watchedEpisodes
- Changes are automatically persisted to localStorage under the key
local-progress-store
markSeasonWatched
Mark all episodes in a season as watched or unwatched.
const markSeasonWatched = useLocalProgressStore(
(state) => state.markSeasonWatched
);
// Mark entire season 1 as watched
const episodeNumbers = [1, 2, 3, 4, 5, 6, 7];
markSeasonWatched(1396, 1, episodeNumbers, true);
// Unmark entire season 1
markSeasonWatched(1396, 1, episodeNumbers, false);
Parameters
Array of episode numbers to mark
true to mark all episodes as watched, false to unmark all
Example Usage
import { useLocalProgressStore } from '@/hooks/useLocalProgressStore';
function SeasonToggleButton({ tvId, seasonNumber, episodeCount }) {
const markSeasonWatched = useLocalProgressStore(
(state) => state.markSeasonWatched
);
const watchedEpisodes = useLocalProgressStore(
(state) => state.watchedEpisodes
);
// Check if all episodes are watched
const episodeNumbers = Array.from(
{ length: episodeCount },
(_, i) => i + 1
);
const allWatched = episodeNumbers.every(
(ep) => watchedEpisodes[`${tvId}:${seasonNumber}:${ep}`]
);
const handleToggle = () => {
markSeasonWatched(tvId, seasonNumber, episodeNumbers, !allWatched);
};
return (
<button onClick={handleToggle}>
{allWatched ? 'Unmark Season' : 'Mark Season Watched'}
</button>
);
}
clearShowProgress
Remove all episode progress data for a specific TV show.
const clearShowProgress = useLocalProgressStore(
(state) => state.clearShowProgress
);
// Clear all progress for Breaking Bad
clearShowProgress(1396);
Parameters
TMDB ID of the TV show to clear
Behavior
Removes all episode entries with keys starting with ${tmdbId}: from the store. Useful for:
- Resetting watch progress for a show
- Cleaning up after show deletion
- Testing and development
Episode keys use a colon-separated format:
${tmdbId}:${season}:${episode}
Examples:
"1396:1:1" - Breaking Bad, Season 1, Episode 1
"82856:2:16" - The Mandalorian, Season 2, Episode 16
"60574:0:1" - Special episode (season 0)
Implementation Details
Persistence Strategy
The store uses Zustand’s persist middleware with the following configuration:
persist(
(set) => ({ /* store implementation */ }),
{
name: 'local-progress-store',
storage: createJSONStorage(() =>
typeof window !== 'undefined'
? window.localStorage
: memoryStorage
),
}
)
Memory Storage Fallback
In SSR or non-browser environments, the store falls back to an in-memory storage implementation to prevent crashes:
const memoryStorage: Storage = (() => {
let store: Record<string, string> = {};
return {
getItem: (name) => (name in store ? store[name] : null),
setItem: (name, value) => { store[name] = String(value); },
removeItem: (name) => { delete store[name]; },
clear: () => { store = {}; },
// ...
};
})();
Integration with useEpisodeWatched
The local progress store is automatically used by useEpisodeWatched when the user is not signed in:
// From useEpisodeWatched hook
const localEpisodes = useLocalProgressStore(
(state) => state.watchedEpisodes
);
const markLocalEpisode = useLocalProgressStore(
(state) => state.markEpisodeWatched
);
if (!isSignedIn) {
// Use local storage
markLocalEpisode(tmdbId, season, episode, isWatched);
} else {
// Use Convex mutation
markEpisodeWatchedMut({ tmdbId, season, episode, isWatched });
}
- Automatic Batching: Zustand automatically batches state updates for optimal performance
- Selective Subscriptions: Use selector functions to subscribe only to needed state:
// Good - only re-renders when watchedEpisodes changes
const episodes = useLocalProgressStore((state) => state.watchedEpisodes);
// Avoid - re-renders on any state change
const store = useLocalProgressStore();
- Storage Size: localStorage has typical limits of 5-10MB. Episode data is very compact (one boolean per episode)
Migration to Authenticated State
When a user signs in, their local progress should be migrated to Convex. This is handled by the import/export system (see useWatchlistImportExport).
Source
Location: ~/workspace/source/src/hooks/useLocalProgressStore.ts
The store is a lightweight Zustand implementation with just 95 lines of code, providing essential episode tracking for guest users.