fix(web): guard RegistrationPanel invite-list against stale-fetch race

This commit is contained in:
Jannis Braun
2026-04-29 00:57:57 +02:00
parent dcd3daf390
commit 42d1be54fa
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import type { InviteLinkSummary } from '@backspace/shared'; import type { InviteLinkSummary } from '@backspace/shared';
import { api } from '../../../api/client'; import { api } from '../../../api/client';
import { useSettingsStore } from '../../../stores/settingsStore'; import { useSettingsStore } from '../../../stores/settingsStore';
@@ -125,16 +125,27 @@ export function RegistrationPanel() {
const [invites, setInvites] = useState<InviteLinkSummary[]>([]); const [invites, setInvites] = useState<InviteLinkSummary[]>([]);
const [invitesLoading, setInvitesLoading] = useState(false); const [invitesLoading, setInvitesLoading] = useState(false);
// Tracks the currently displayed tab so in-flight fetches can detect when
// the user has switched tabs and discard their stale response. Without this
// guard, a slower 'archived' response can resolve after a newer 'active'
// response and clobber the visible list. Used by both the auto-load effect
// and manual fetchInvites() callers (e.g. post-mutation refresh).
const tabRef = useRef<'active' | 'archived'>(tab);
useEffect(() => {
tabRef.current = tab;
}, [tab]);
const fetchInvites = useCallback( const fetchInvites = useCallback(
async (which: 'active' | 'archived') => { async (which: 'active' | 'archived') => {
setInvitesLoading(true); setInvitesLoading(true);
try { try {
const res = await api.invites.list(which); const res = await api.invites.list(which);
if (tabRef.current !== which) return;
setInvites(res.invites); setInvites(res.invites);
} catch { } catch {
addToast('Failed to load invites', 'warning'); if (tabRef.current === which) addToast('Failed to load invites', 'warning');
} finally { } finally {
setInvitesLoading(false); if (tabRef.current === which) setInvitesLoading(false);
} }
}, },
[addToast], [addToast],