Merge pull request #13 from purrgrammer/claude/useprofile-race-fix-EeWQZ

fix: prevent race conditions in useProfile hook
This commit is contained in:
Alejandro
2025-12-22 13:12:25 +01:00
committed by GitHub

View File

@@ -1,19 +1,38 @@
import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
import { profileLoader } from "@/services/loaders";
import { ProfileContent, getProfileContent } from "applesauce-core/helpers";
import { kinds } from "nostr-tools";
import db from "@/services/db";
/**
* Hook to fetch and cache user profile metadata
*
* Uses AbortController to prevent race conditions when:
* - Component unmounts during async operations
* - Pubkey changes while a fetch is in progress
*
* @param pubkey - The user's public key (hex)
* @returns ProfileContent or undefined if loading/not found
*/
export function useProfile(pubkey?: string): ProfileContent | undefined {
const [profile, setProfile] = useState<ProfileContent | undefined>();
const abortControllerRef = useRef<AbortController | null>(null);
useEffect(() => {
let mounted = true;
if (!pubkey) return;
if (!pubkey) {
setProfile(undefined);
return;
}
// Load from IndexedDB first
// Abort any in-flight requests from previous effect runs
abortControllerRef.current?.abort();
const controller = new AbortController();
abortControllerRef.current = controller;
// Load from IndexedDB first (fast path)
db.profiles.get(pubkey).then((cachedProfile) => {
if (mounted && cachedProfile) {
if (controller.signal.aborted) return;
if (cachedProfile) {
setProfile(cachedProfile);
}
});
@@ -21,6 +40,7 @@ export function useProfile(pubkey?: string): ProfileContent | undefined {
// Fetch from network
const sub = profileLoader({ kind: kinds.Metadata, pubkey }).subscribe({
next: async (fetchedEvent) => {
if (controller.signal.aborted) return;
if (!fetchedEvent || !fetchedEvent.content) return;
// Use applesauce helper for safe profile parsing
@@ -30,24 +50,31 @@ export function useProfile(pubkey?: string): ProfileContent | undefined {
return;
}
// Save to IndexedDB
await db.profiles.put({
...profileData,
pubkey,
created_at: fetchedEvent.created_at,
});
// Only update state and cache if not aborted
if (controller.signal.aborted) return;
if (mounted) {
setProfile(profileData);
setProfile(profileData);
// Save to IndexedDB after state update to avoid blocking UI
try {
await db.profiles.put({
...profileData,
pubkey,
created_at: fetchedEvent.created_at,
});
} catch (err) {
// Log but don't throw - cache failure shouldn't break the UI
console.error("[useProfile] Failed to cache profile:", err);
}
},
error: (err) => {
if (controller.signal.aborted) return;
console.error("[useProfile] Error fetching profile:", err);
},
});
return () => {
mounted = false;
controller.abort();
sub.unsubscribe();
};
}, [pubkey]);