diff --git a/frontend/src/features/follows/TagFollowToggle.tsx b/frontend/src/features/follows/TagFollowToggle.tsx index d4248ca..581a8f6 100644 --- a/frontend/src/features/follows/TagFollowToggle.tsx +++ b/frontend/src/features/follows/TagFollowToggle.tsx @@ -11,6 +11,8 @@ export function TagFollowToggle({ tag }: { tag: string }) { const user = useAuth((state) => state.user); const queryClient = useQueryClient(); const [error, setError] = useState(null); + // What the chip should show once the request and refetch settle. + const [target, setTarget] = useState(null); const query = useQuery({ queryKey: ["follows-tags"], @@ -37,9 +39,17 @@ export function TagFollowToggle({ tag }: { tag: string }) { void queryClient.invalidateQueries({ queryKey: ["follows-feed"] }); void queryClient.invalidateQueries({ queryKey: ["follows-cloud"] }); }, - onError: (err) => setError(errorMessage(err)), + onError: (err) => { + setTarget(null); + setError(errorMessage(err)); + }, }); + // Keep the spinner up until the refetched list reflects the new state. + // A stale target that matches `followed` is harmless: the next click + // recomputes it before mutating. + const busy = mutation.isPending || (target !== null && followed !== target); + if (!user) return null; function handleClick(event: MouseEvent) { @@ -47,6 +57,8 @@ export function TagFollowToggle({ tag }: { tag: string }) { // action and must not trigger them. event.preventDefault(); event.stopPropagation(); + if (busy) return; + setTarget(!followed); mutation.mutate(); } @@ -54,16 +66,21 @@ export function TagFollowToggle({ tag }: { tag: string }) {