-
Notifications
You must be signed in to change notification settings - Fork 8
Description
Migrate project storage from localStorage to IndexedDB
Summary
Migrate the project storage mechanism from localStorage to IndexedDB using Dexie.js with the new useSuspendingLiveQuery hook for React Suspense integration.
Background
Currently, projects are stored in localStorage via the useLocalStorageState hook (app/src/app/utils/useLocalStorageState.ts). While functional, localStorage has limitations:
- 5MB storage limit per origin
- Synchronous API blocks the main thread
- String-only storage requires JSON serialization/deserialization
- No indexing for efficient queries
IndexedDB provides:
- Much larger storage (typically 50%+ of available disk space)
- Asynchronous API for better performance
- Native object storage without serialization overhead
- Indexed queries for efficient lookups
- Transaction support for data integrity
Library Choice: Dexie.js with useSuspendingLiveQuery
We'll use Dexie.js with the new useSuspendingLiveQuery hook for native React Suspense support.
Why this approach:
- Dexie.js - Popular IndexedDB wrapper (11k+ GitHub stars) with excellent TypeScript support
useSuspendingLiveQuery- Official Suspense-enabled hook (merged Nov 2025)- Reactive updates - Automatically re-renders when data changes
- No custom hooks needed - Uses official Dexie implementation
Implementation Plan
Phase 1: Install Dependencies
pnpm add dexie [email protected]Note:
useSuspendingLiveQueryis available in the alpha release. Pin to this version until stable release.
Phase 2: Create Database Schema
2.1 Create database definition
- File:
app/src/app/db/database.ts
import Dexie, { type EntityTable } from 'dexie';
import type { Project } from '../actions/project';
const db = new Dexie('RedditHarbor') as Dexie & {
projects: EntityTable<Project, 'id'>;
};
db.version(1).stores({
projects: 'id, createdAt', // 'id' is primary key, 'createdAt' is indexed
});
export { db };Phase 3: Update useProjects Hook
3.1 Rewrite useProjects.ts with useSuspendingLiveQuery
- File:
app/src/app/dashboard/useProjects.ts
import { useSuspendingLiveQuery } from 'dexie-react-hooks';
import { db } from '../db/database';
import type { Project } from '../actions/project';
export function useProjects() {
const projects = useSuspendingLiveQuery(
'projects-list',
() => db.projects.toArray(),
[]
);
async function addProject(project: Project) {
await db.projects.add(project);
}
async function updateProject(project: Project) {
await db.projects.put(project);
}
async function deleteProject(project: Project) {
await db.projects.delete(project.id);
}
return [projects, { addProject, updateProject, deleteProject }] as const;
}Phase 4: Add Suspense Boundaries and Error Handling
4.1 Create error fallback component
- File:
app/src/app/dashboard/ProjectsErrorFallback.tsx
'use client';
import { type FallbackProps } from 'react-error-boundary';
import Alert from '@mui/material/Alert';
import Button from '@mui/material/Button';
export function ProjectsErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<Alert
severity="error"
action={
<Button color="inherit" size="small" onClick={resetErrorBoundary}>
Retry
</Button>
}
>
{Error.isError(error) ? `${error}` : 'Failed to load projects'}
</Alert>
);
}4.2 Update Dashboard page with Suspense + ErrorBoundary
- File:
app/src/app/dashboard/page.tsx
import { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { ProjectCards } from './ProjectCards';
import { ProjectsErrorFallback } from './ProjectsErrorFallback';
export default function DashboardPage() {
return (
<ErrorBoundary FallbackComponent={ProjectsErrorFallback}>
<Suspense fallback={null}>
<ProjectCards />
</Suspense>
</ErrorBoundary>
);
}Phase 5: Cleanup
- Remove
'projects'localStorage key usage - Remove TODO comment from
useProjects.ts - Keep
useLocalStorageState.ts(still used for API keys)
Files to Create/Modify
| File | Action | Description |
|---|---|---|
app/src/app/db/database.ts |
Create | Dexie database definition |
app/src/app/dashboard/useProjects.ts |
Modify | Use useSuspendingLiveQuery |
app/src/app/dashboard/page.tsx |
Modify | Add Suspense + ErrorBoundary |
app/src/app/dashboard/ProjectsErrorFallback.tsx |
Create | Error fallback UI |
Technical Considerations
useSuspendingLiveQuery API
useSuspendingLiveQuery<T>(
cacheKey: string, // Globally unique cache key
querier: () => T | Promise<T>, // Query function
deps: unknown[] // Dependency array (like useEffect)
): TImportant: The cacheKey must be globally unique across your application. If two different queries share the same cache key, they may return incorrect cached data.
Error Handling
- Wrap Suspense in ErrorBoundary for graceful error handling
- IndexedDB can fail in private browsing mode
- Consider showing user-friendly error messages
Alpha Version Note
useSuspendingLiveQueryis in[email protected]- Monitor for stable release and update when available
- API may change slightly before stable release
API Changes
// Before (synchronous localStorage)
const [projects, { addProject }] = useProjects();
addProject(project); // sync
// After (Suspense + async operations)
// Component wrapped in <Suspense fallback={...}>
const [projects, { addProject }] = useProjects(); // projects is always defined (Suspense handles loading)
await addProject(project); // asyncOut of Scope
Migration of existing localStorage data(per user preference)- Syncing projects across devices (future enhancement)
- Offline-first architecture (future enhancement)
Testing Checklist
- Create, update, and delete projects - verify persistence after refresh
- Test error state: Open in private/incognito mode to trigger IndexedDB error
- Verify "Retry" button works in error boundary
References
- Dexie.js Documentation
- useSuspendingLiveQuery PR #2205
- Dexie React Hooks
- Current implementation:
app/src/app/dashboard/useProjects.ts