Defer profile featured artworks

This commit is contained in:
2026-08-29 16:33:36 +02:00
parent ff80f5bf97
commit 872f420190
7 changed files with 336 additions and 39 deletions
@@ -0,0 +1,83 @@
import { useCallback, useEffect, useRef, useState } from 'react'
export default function useLazyProfileFeaturedArtworks({ username, initialFeaturedArtworks, enabled }) {
const hasInitialValue = Array.isArray(initialFeaturedArtworks)
const [items, setItems] = useState(() => hasInitialValue ? initialFeaturedArtworks : [])
const [loaded, setLoaded] = useState(hasInitialValue)
const [loading, setLoading] = useState(false)
const [error, setError] = useState(false)
const requestKeyRef = useRef(username)
const requestedRef = useRef(hasInitialValue)
const abortRef = useRef(null)
useEffect(() => {
if (requestKeyRef.current === username) return undefined
abortRef.current?.abort()
abortRef.current = null
requestKeyRef.current = username
requestedRef.current = false
setItems([])
setLoaded(false)
setLoading(false)
setError(false)
return undefined
}, [username])
useEffect(() => {
if (!hasInitialValue || requestKeyRef.current !== username) return
abortRef.current?.abort()
abortRef.current = null
requestedRef.current = true
setItems(initialFeaturedArtworks)
setLoaded(true)
setLoading(false)
setError(false)
}, [hasInitialValue, initialFeaturedArtworks, username])
const load = useCallback(async () => {
if (!enabled || hasInitialValue || requestedRef.current || !username) return
requestedRef.current = true
const controller = new AbortController()
const requestKey = requestKeyRef.current
abortRef.current = controller
setLoading(true)
setError(false)
try {
const response = await fetch(`/api/profile/${encodeURIComponent(username)}/featured-artworks`, {
headers: { Accept: 'application/json' },
credentials: 'same-origin',
signal: controller.signal,
})
if (!response.ok) throw new Error('Featured artworks could not be loaded.')
const payload = await response.json()
if (requestKey !== requestKeyRef.current || controller.signal.aborted) return
setItems(Array.isArray(payload?.data) ? payload.data : [])
setLoaded(true)
} catch (caught) {
if (caught?.name === 'AbortError' || requestKey !== requestKeyRef.current) return
requestedRef.current = false
setError(true)
} finally {
if (requestKey === requestKeyRef.current) {
abortRef.current = null
setLoading(false)
}
}
}, [enabled, hasInitialValue, username])
useEffect(() => {
if (enabled) load()
}, [enabled, load])
useEffect(() => () => abortRef.current?.abort(), [])
return { items, loaded, loading, error, retry: load }
}
@@ -0,0 +1,77 @@
import React from 'react'
import { act, render, screen, waitFor } from '@testing-library/react'
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
import useLazyProfileFeaturedArtworks from './useLazyProfileFeaturedArtworks'
function Harness({ username = 'alice', initial = null, enabled = false }) {
const state = useLazyProfileFeaturedArtworks({
username,
initialFeaturedArtworks: initial,
enabled,
})
return (
<div>
<span data-testid="count">{state.items.length}</span>
<span data-testid="loading">{String(state.loading)}</span>
<span data-testid="error">{String(state.error)}</span>
<button type="button" onClick={state.retry}>retry</button>
{state.items.map((item) => <span key={item.id}>{item.title}</span>)}
</div>
)
}
describe('useLazyProfileFeaturedArtworks', () => {
beforeEach(() => {
vi.stubGlobal('fetch', vi.fn(async () => ({
ok: true,
json: async () => ({ data: [{ id: 1, title: 'Featured one' }] }),
})))
})
afterEach(() => vi.unstubAllGlobals())
it('does not request while the posts tab is active', () => {
render(<Harness />)
expect(fetch).not.toHaveBeenCalled()
})
it('loads once on first artworks activation and reuses the result', async () => {
const view = render(<Harness enabled />)
await waitFor(() => expect(screen.getByText('Featured one')).toBeTruthy())
expect(fetch).toHaveBeenCalledTimes(1)
view.rerender(<Harness enabled={false} />)
view.rerender(<Harness enabled />)
expect(fetch).toHaveBeenCalledTimes(1)
})
it('does not request when SSR already supplied featured artworks', () => {
render(<Harness enabled initial={[{ id: 9, title: 'SSR feature' }]} />)
expect(screen.getByText('SSR feature')).toBeTruthy()
expect(fetch).not.toHaveBeenCalled()
})
it('allows a safe retry after failure', async () => {
fetch.mockRejectedValueOnce(new Error('network'))
render(<Harness enabled />)
await waitFor(() => expect(screen.getByTestId('error').textContent).toBe('true'))
await act(async () => screen.getByRole('button', { name: 'retry' }).click())
await waitFor(() => expect(screen.getByText('Featured one')).toBeTruthy())
expect(fetch).toHaveBeenCalledTimes(2)
})
it('ignores a stale response after profile navigation', async () => {
let resolveAlice
fetch.mockImplementationOnce(() => new Promise((resolve) => { resolveAlice = resolve }))
const view = render(<Harness username="alice" enabled />)
view.rerender(<Harness username="bob" enabled={false} />)
await act(async () => resolveAlice({ ok: true, json: async () => ({ data: [{ id: 1, title: 'Alice feature' }] }) }))
expect(screen.queryByText('Alice feature')).toBeNull()
expect(screen.getByTestId('count').textContent).toBe('0')
})
})