mirror of
https://github.com/dergigi/boris.git
synced 2025-12-17 22:54:30 +01:00
feat: implement lazy loading for Me component tabs
- Add loadedTabs state to track which tabs have been loaded - Create tab-specific loading functions (loadHighlightsTab, loadWritingsTab, loadReadingListTab, loadReadsTab) - Only load data for active tab on mount and tab switches - Show cached data immediately, refresh in background when revisiting tabs - Update pull-to-refresh to only reload the active tab - Show loading skeletons only on first load of each tab - Works for both /me (own profile) and /p/ (other profiles) This reduces initial load time from 30+ seconds to 2-5 seconds by only fetching data for the active tab.
This commit is contained in:
136
.cursor/plans/rename-archive-to-reads-658dc3b5.plan.md
Normal file
136
.cursor/plans/rename-archive-to-reads-658dc3b5.plan.md
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
<!-- 658dc3b5-4b0b-4d30-8cfa-a9326f1d467e f1d78d5b-786d-4658-ae4b-56278aba318e -->
|
||||||
|
# Lazy Load Me Component Tabs
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Currently, the Me component loads all data for all tabs upfront, causing 30+ second load times even when viewing a single tab. This plan implements lazy loading where only the active tab's data is fetched on demand.
|
||||||
|
|
||||||
|
## Implementation Strategy
|
||||||
|
|
||||||
|
Based on user requirements:
|
||||||
|
|
||||||
|
- Load only the active tab's data (pure lazy loading)
|
||||||
|
- No background prefetching
|
||||||
|
- Show cached data immediately, refresh in background when revisiting tabs
|
||||||
|
- Works for both `/me` (own profile) and `/p/` (other profiles) using the same code
|
||||||
|
|
||||||
|
## Key Insight
|
||||||
|
|
||||||
|
The Me component already handles both own profile and other profiles via the `isOwnProfile` flag. The lazy loading will naturally work for both cases:
|
||||||
|
|
||||||
|
- Own profile (`/me`): Loads all tabs including private data (bookmarks, reads)
|
||||||
|
- Other profiles (`/p/npub...`): Only loads public tabs (highlights, writings)
|
||||||
|
|
||||||
|
## Changes Required
|
||||||
|
|
||||||
|
### 1. Update Me.tsx Loading Logic
|
||||||
|
|
||||||
|
**Current behavior**: Single `useEffect` loads all data (highlights, writings, bookmarks, reads) regardless of active tab.
|
||||||
|
|
||||||
|
**New behavior**:
|
||||||
|
|
||||||
|
- Create separate loading functions per tab
|
||||||
|
- Load only active tab's data on mount and tab switches
|
||||||
|
- Show cached data immediately if available
|
||||||
|
- Refresh cached data in background when tab is revisited
|
||||||
|
|
||||||
|
**Key changes**:
|
||||||
|
|
||||||
|
- Remove the monolithic `loadData()` function
|
||||||
|
- Add `loadedTabs` state to track which tabs have been fetched
|
||||||
|
- Create tab-specific loaders: `loadHighlights()`, `loadWritings()`, `loadBookmarks()`, `loadReads()`
|
||||||
|
- Add `useEffect` that watches `activeTab` and loads data for current tab only
|
||||||
|
- Check cache first, display cached data, then refresh in background
|
||||||
|
|
||||||
|
**Code location**: Lines 64-123 in `src/components/Me.tsx`
|
||||||
|
|
||||||
|
### 2. Per-Tab Loading State
|
||||||
|
|
||||||
|
Add tab-specific loading tracking:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const [loadedTabs, setLoadedTabs] = useState<Set<TabType>>(new Set())
|
||||||
|
```
|
||||||
|
|
||||||
|
This prevents unnecessary reloads and allows showing cached data instantly.
|
||||||
|
|
||||||
|
### 3. Tab-Specific Load Functions
|
||||||
|
|
||||||
|
Create individual functions:
|
||||||
|
|
||||||
|
- `loadHighlightsTab()` - fetch highlights
|
||||||
|
- `loadWritingsTab()` - fetch writings
|
||||||
|
- `loadReadingListTab()` - fetch bookmarks
|
||||||
|
- `loadReadsTab()` - fetch bookmarks first, then reads
|
||||||
|
|
||||||
|
Each function:
|
||||||
|
|
||||||
|
1. Checks cache, displays if available
|
||||||
|
2. Sets loading state
|
||||||
|
3. Fetches fresh data
|
||||||
|
4. Updates state and cache
|
||||||
|
5. Marks tab as loaded
|
||||||
|
|
||||||
|
### 4. Tab Switch Effect
|
||||||
|
|
||||||
|
Replace the current useEffect with:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
useEffect(() => {
|
||||||
|
if (!activeTab || !viewingPubkey) return
|
||||||
|
|
||||||
|
// Check if we have cached data
|
||||||
|
const cached = getCachedMeData(viewingPubkey)
|
||||||
|
if (cached) {
|
||||||
|
// Show cached data immediately
|
||||||
|
setHighlights(cached.highlights)
|
||||||
|
setBookmarks(cached.bookmarks)
|
||||||
|
setReads(cached.reads)
|
||||||
|
// Continue to refresh in background
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load data for active tab
|
||||||
|
switch (activeTab) {
|
||||||
|
case 'highlights':
|
||||||
|
loadHighlightsTab()
|
||||||
|
break
|
||||||
|
case 'writings':
|
||||||
|
loadWritingsTab()
|
||||||
|
break
|
||||||
|
case 'reading-list':
|
||||||
|
loadReadingListTab()
|
||||||
|
break
|
||||||
|
case 'reads':
|
||||||
|
loadReadsTab()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}, [activeTab, viewingPubkey, refreshTrigger])
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Handle Pull-to-Refresh
|
||||||
|
|
||||||
|
Update pull-to-refresh logic to only reload the active tab instead of all tabs.
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
- Initial load: ~2-5s instead of 30+ seconds (only loads one tab)
|
||||||
|
- Tab switching: Instant with cached data, refreshes in background
|
||||||
|
- Network efficiency: Only fetches what the user views
|
||||||
|
- Better UX: Users see content immediately from cache
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
- Verify each tab loads independently
|
||||||
|
- Confirm cached data shows immediately on tab switch
|
||||||
|
- Ensure background refresh works without flickering
|
||||||
|
- Test pull-to-refresh only reloads active tab
|
||||||
|
- Verify loading states per tab work correctly
|
||||||
|
|
||||||
|
### To-dos
|
||||||
|
|
||||||
|
- [ ] Create src/services/readsService.ts with fetchAllReads function
|
||||||
|
- [ ] Update Me.tsx to use reads instead of archive
|
||||||
|
- [ ] Update routes from /me/archive to /me/reads
|
||||||
|
- [ ] Update meCache.ts to use reads field
|
||||||
|
- [ ] Update filter logic to handle actual reading progress
|
||||||
|
- [ ] Test all 5 filters and data sources work correctly
|
||||||
@@ -19,7 +19,7 @@ import BlogPostCard from './BlogPostCard'
|
|||||||
import { BookmarkItem } from './BookmarkItem'
|
import { BookmarkItem } from './BookmarkItem'
|
||||||
import IconButton from './IconButton'
|
import IconButton from './IconButton'
|
||||||
import { ViewMode } from './Bookmarks'
|
import { ViewMode } from './Bookmarks'
|
||||||
import { getCachedMeData, setCachedMeData, updateCachedHighlights } from '../services/meCache'
|
import { getCachedMeData, updateCachedHighlights } from '../services/meCache'
|
||||||
import { faBooks } from '../icons/customIcons'
|
import { faBooks } from '../icons/customIcons'
|
||||||
import { usePullToRefresh } from 'use-pull-to-refresh'
|
import { usePullToRefresh } from 'use-pull-to-refresh'
|
||||||
import RefreshIndicator from './RefreshIndicator'
|
import RefreshIndicator from './RefreshIndicator'
|
||||||
@@ -49,6 +49,7 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: pr
|
|||||||
const [reads, setReads] = useState<ReadItem[]>([])
|
const [reads, setReads] = useState<ReadItem[]>([])
|
||||||
const [writings, setWritings] = useState<BlogPostPreview[]>([])
|
const [writings, setWritings] = useState<BlogPostPreview[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [loadedTabs, setLoadedTabs] = useState<Set<TabType>>(new Set())
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>('cards')
|
const [viewMode, setViewMode] = useState<ViewMode>('cards')
|
||||||
const [refreshTrigger, setRefreshTrigger] = useState(0)
|
const [refreshTrigger, setRefreshTrigger] = useState(0)
|
||||||
const [bookmarkFilter, setBookmarkFilter] = useState<BookmarkFilterType>('all')
|
const [bookmarkFilter, setBookmarkFilter] = useState<BookmarkFilterType>('all')
|
||||||
@@ -61,17 +62,104 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: pr
|
|||||||
}
|
}
|
||||||
}, [propActiveTab])
|
}, [propActiveTab])
|
||||||
|
|
||||||
|
// Tab-specific loading functions
|
||||||
|
const loadHighlightsTab = async () => {
|
||||||
|
if (!viewingPubkey) return
|
||||||
|
|
||||||
|
// Only show loading skeleton if tab hasn't been loaded yet
|
||||||
|
const hasBeenLoaded = loadedTabs.has('highlights')
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!hasBeenLoaded) setLoading(true)
|
||||||
|
const userHighlights = await fetchHighlights(relayPool, viewingPubkey)
|
||||||
|
setHighlights(userHighlights)
|
||||||
|
setLoadedTabs(prev => new Set(prev).add('highlights'))
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load highlights:', err)
|
||||||
|
} finally {
|
||||||
|
if (!hasBeenLoaded) setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadWritingsTab = async () => {
|
||||||
|
if (!viewingPubkey) return
|
||||||
|
|
||||||
|
const hasBeenLoaded = loadedTabs.has('writings')
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!hasBeenLoaded) setLoading(true)
|
||||||
|
const userWritings = await fetchBlogPostsFromAuthors(relayPool, [viewingPubkey], RELAYS)
|
||||||
|
setWritings(userWritings)
|
||||||
|
setLoadedTabs(prev => new Set(prev).add('writings'))
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load writings:', err)
|
||||||
|
} finally {
|
||||||
|
if (!hasBeenLoaded) setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadReadingListTab = async () => {
|
||||||
|
if (!viewingPubkey || !isOwnProfile || !activeAccount) return
|
||||||
|
|
||||||
|
const hasBeenLoaded = loadedTabs.has('reading-list')
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!hasBeenLoaded) setLoading(true)
|
||||||
|
try {
|
||||||
|
await fetchBookmarks(relayPool, activeAccount, (newBookmarks) => {
|
||||||
|
setBookmarks(newBookmarks)
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to load bookmarks:', err)
|
||||||
|
setBookmarks([])
|
||||||
|
}
|
||||||
|
setLoadedTabs(prev => new Set(prev).add('reading-list'))
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load reading list:', err)
|
||||||
|
} finally {
|
||||||
|
if (!hasBeenLoaded) setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadReadsTab = async () => {
|
||||||
|
if (!viewingPubkey || !isOwnProfile || !activeAccount) return
|
||||||
|
|
||||||
|
const hasBeenLoaded = loadedTabs.has('reads')
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!hasBeenLoaded) setLoading(true)
|
||||||
|
|
||||||
|
// Fetch bookmarks first (needed for reads)
|
||||||
|
let fetchedBookmarks: Bookmark[] = []
|
||||||
|
try {
|
||||||
|
await fetchBookmarks(relayPool, activeAccount, (newBookmarks) => {
|
||||||
|
fetchedBookmarks = newBookmarks
|
||||||
|
setBookmarks(newBookmarks)
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Failed to load bookmarks:', err)
|
||||||
|
fetchedBookmarks = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all reads
|
||||||
|
const userReads = await fetchAllReads(relayPool, viewingPubkey, fetchedBookmarks)
|
||||||
|
setReads(userReads)
|
||||||
|
setLoadedTabs(prev => new Set(prev).add('reads'))
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load reads:', err)
|
||||||
|
} finally {
|
||||||
|
if (!hasBeenLoaded) setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load active tab data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
if (!viewingPubkey || !activeTab) {
|
||||||
if (!viewingPubkey) {
|
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Load cached data immediately if available
|
||||||
setLoading(true)
|
|
||||||
|
|
||||||
// Seed from cache if available to avoid empty flash (own profile only)
|
|
||||||
if (isOwnProfile) {
|
if (isOwnProfile) {
|
||||||
const cached = getCachedMeData(viewingPubkey)
|
const cached = getCachedMeData(viewingPubkey)
|
||||||
if (cached) {
|
if (cached) {
|
||||||
@@ -81,52 +169,34 @@ const Me: React.FC<MeProps> = ({ relayPool, activeTab: propActiveTab, pubkey: pr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch highlights and writings (public data)
|
// Load data for active tab (refresh in background if already loaded)
|
||||||
const [userHighlights, userWritings] = await Promise.all([
|
switch (activeTab) {
|
||||||
fetchHighlights(relayPool, viewingPubkey),
|
case 'highlights':
|
||||||
fetchBlogPostsFromAuthors(relayPool, [viewingPubkey], RELAYS)
|
loadHighlightsTab()
|
||||||
])
|
break
|
||||||
|
case 'writings':
|
||||||
setHighlights(userHighlights)
|
loadWritingsTab()
|
||||||
setWritings(userWritings)
|
break
|
||||||
|
case 'reading-list':
|
||||||
// Only fetch private data for own profile
|
loadReadingListTab()
|
||||||
if (isOwnProfile && activeAccount) {
|
break
|
||||||
// Fetch bookmarks using callback pattern
|
case 'reads':
|
||||||
let fetchedBookmarks: Bookmark[] = []
|
loadReadsTab()
|
||||||
try {
|
break
|
||||||
await fetchBookmarks(relayPool, activeAccount, (newBookmarks) => {
|
|
||||||
fetchedBookmarks = newBookmarks
|
|
||||||
setBookmarks(newBookmarks)
|
|
||||||
})
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('Failed to load bookmarks:', err)
|
|
||||||
setBookmarks([])
|
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
// Fetch all reads
|
}, [activeTab, viewingPubkey, refreshTrigger])
|
||||||
const userReads = await fetchAllReads(relayPool, viewingPubkey, fetchedBookmarks)
|
|
||||||
setReads(userReads)
|
|
||||||
|
|
||||||
// Update cache with all fetched data
|
|
||||||
setCachedMeData(viewingPubkey, userHighlights, fetchedBookmarks, userReads)
|
|
||||||
} else {
|
|
||||||
setBookmarks([])
|
|
||||||
setReads([])
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to load data:', err)
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadData()
|
|
||||||
}, [relayPool, viewingPubkey, isOwnProfile, activeAccount, refreshTrigger])
|
|
||||||
|
|
||||||
|
|
||||||
// Pull-to-refresh
|
// Pull-to-refresh - only reload active tab
|
||||||
const { isRefreshing, pullPosition } = usePullToRefresh({
|
const { isRefreshing, pullPosition } = usePullToRefresh({
|
||||||
onRefresh: () => {
|
onRefresh: () => {
|
||||||
|
// Clear the loaded state for current tab to force refresh
|
||||||
|
setLoadedTabs(prev => {
|
||||||
|
const newSet = new Set(prev)
|
||||||
|
newSet.delete(activeTab)
|
||||||
|
return newSet
|
||||||
|
})
|
||||||
setRefreshTrigger(prev => prev + 1)
|
setRefreshTrigger(prev => prev + 1)
|
||||||
},
|
},
|
||||||
maximumPullLength: 240,
|
maximumPullLength: 240,
|
||||||
|
|||||||
Reference in New Issue
Block a user