Defer profile world history until needed
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
export default function useLazyProfileWorldHistory({ username, initialWorldHistory, enabled }) {
|
||||
const hasInitialValue = initialWorldHistory !== null && initialWorldHistory !== undefined
|
||||
const [data, setData] = useState(() => hasInitialValue ? initialWorldHistory : null)
|
||||
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
|
||||
setData(null)
|
||||
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
|
||||
setData(initialWorldHistory)
|
||||
setLoaded(true)
|
||||
setLoading(false)
|
||||
setError(false)
|
||||
}, [hasInitialValue, initialWorldHistory, username])
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!enabled || hasInitialValue || requestKeyRef.current !== username || 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)}/world-history`, {
|
||||
headers: { Accept: 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
signal: controller.signal,
|
||||
})
|
||||
if (!response.ok) throw new Error('World history could not be loaded.')
|
||||
const payload = await response.json()
|
||||
if (requestKey !== requestKeyRef.current || controller.signal.aborted) return
|
||||
setData(payload)
|
||||
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 { data, loaded, loading, error, retry: load }
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import React from 'react'
|
||||
import { render, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import useLazyProfileWorldHistory from './useLazyProfileWorldHistory'
|
||||
|
||||
function Harness({ username = 'alice', initial = null, enabled = false }) {
|
||||
const state = useLazyProfileWorldHistory({
|
||||
username,
|
||||
initialWorldHistory: initial,
|
||||
enabled,
|
||||
})
|
||||
|
||||
return <output data-testid="state">{JSON.stringify(state)}</output>
|
||||
}
|
||||
|
||||
describe('useLazyProfileWorldHistory', () => {
|
||||
beforeEach(() => vi.restoreAllMocks())
|
||||
|
||||
it('does not request on Posts until a consuming tab is active', () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch')
|
||||
render(<Harness />)
|
||||
expect(fetchMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('requests once on first consuming-tab activation and reuses the result', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ summary: { available: true }, entries: [{ id: 'world-1' }] }),
|
||||
})
|
||||
const view = render(<Harness />)
|
||||
|
||||
view.rerender(<Harness enabled />)
|
||||
await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1))
|
||||
view.rerender(<Harness enabled={false} />)
|
||||
view.rerender(<Harness enabled />)
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
await waitFor(() => expect(JSON.parse(view.getByTestId('state').textContent).loaded).toBe(true))
|
||||
})
|
||||
|
||||
it('uses direct Worlds/Achievements SSR data without a lazy request', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch')
|
||||
const initial = { summary: { available: true }, entries: [{ id: 'world-1' }] }
|
||||
const view = render(<Harness initial={initial} enabled />)
|
||||
view.rerender(<Harness initial={initial} enabled={false} />)
|
||||
view.rerender(<Harness initial={initial} enabled />)
|
||||
|
||||
await waitFor(() => expect(JSON.parse(view.getByTestId('state').textContent).data).toEqual(initial))
|
||||
expect(fetchMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('treats an empty response as loaded', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: true, json: async () => ({ summary: { available: false }, entries: [] }) })
|
||||
const view = render(<Harness enabled />)
|
||||
await waitFor(() => expect(JSON.parse(view.getByTestId('state').textContent).loaded).toBe(true))
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('resets on profile navigation and ignores the stale response', async () => {
|
||||
let resolveA
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(() => new Promise((resolve) => { resolveA = resolve }))
|
||||
const view = render(<Harness username="alice" enabled />)
|
||||
await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1))
|
||||
|
||||
view.rerender(<Harness username="bob" enabled />)
|
||||
resolveA({ ok: true, json: async () => ({ summary: { available: true }, entries: [{ id: 'alice' }] }) })
|
||||
await waitFor(() => expect(JSON.parse(view.getByTestId('state').textContent).data).toBe(null))
|
||||
})
|
||||
|
||||
it('does not loop after an error and permits an explicit retry', async () => {
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch')
|
||||
.mockResolvedValueOnce({ ok: false })
|
||||
.mockResolvedValueOnce({ ok: true, json: async () => ({ summary: { available: false }, entries: [] }) })
|
||||
const view = render(<Harness enabled />)
|
||||
await waitFor(() => expect(JSON.parse(view.getByTestId('state').textContent).error).toBe(true))
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
// The hook's returned retry is intentionally exercised through a small rerender activation.
|
||||
view.rerender(<Harness enabled={false} />)
|
||||
view.rerender(<Harness enabled />)
|
||||
await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(2))
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user