Files
grimoire/src/hooks/useTimeline.ts
Claude bdfc634c54 feat: add dependency stabilization hooks
- Create src/hooks/useStable.ts with:
  - useStableValue<T>() - stabilizes any value using JSON.stringify
  - useStableArray<T>() - stabilizes string arrays (uses JSON.stringify
    for safety, handles arrays with commas in elements)
  - useStableFilters<T>() - specialized for Nostr filters

- Update timeline hooks to use stabilization:
  - useTimeline.ts - use useStableFilters for filter dependencies
  - useReqTimeline.ts - use useStableValue for filter dependencies
  - useLiveTimeline.ts - use useStableArray for relay dependencies

Prevents unnecessary re-renders and subscription restarts when
filter/relay objects are recreated with the same content.
2025-12-22 12:00:42 +00:00

86 lines
2.3 KiB
TypeScript

import { useState, useEffect } from "react";
import type { NostrEvent, Filter } from "nostr-tools";
import { useEventStore, useObservableMemo } from "applesauce-react/hooks";
import { createTimelineLoader } from "@/services/loaders";
import pool from "@/services/relay-pool";
import { AGGREGATOR_RELAYS } from "@/services/loaders";
import { useStableValue, useStableArray } from "./useStable";
interface UseTimelineOptions {
limit?: number;
}
interface UseTimelineReturn {
events: NostrEvent[];
loading: boolean;
error: Error | null;
}
/**
* Hook for subscribing to a timeline of events from relays
* Uses applesauce loaders for efficient event loading and caching
* @param id - Unique identifier for this timeline (for caching)
* @param filters - Nostr filter object
* @param relays - Array of relay URLs
* @param options - Additional options like limit
* @returns Object containing events array, loading state, and error
*/
export function useTimeline(
id: string,
filters: Filter | Filter[],
relays: string[],
options: UseTimelineOptions = { limit: 20 },
): UseTimelineReturn {
const { limit } = options;
const eventStore = useEventStore();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
// Stabilize filters and relays to prevent unnecessary re-renders
const stableFilters = useStableValue(filters);
const stableRelays = useStableArray(relays);
// Load events into store
useEffect(() => {
if (relays.length === 0) return;
const loader = createTimelineLoader(
pool,
relays.concat(AGGREGATOR_RELAYS),
filters,
{
eventStore,
limit,
},
);
setLoading(true);
setError(null);
const subscription = loader().subscribe({
error: (err: Error) => {
console.error("Timeline error:", err);
setError(err);
setLoading(false);
},
complete: () => {
setLoading(false);
},
});
return () => subscription.unsubscribe();
}, [id, stableRelays, limit, eventStore, stableFilters]);
// Watch store for matching events
const timeline = useObservableMemo(() => {
return eventStore.timeline(filters, false);
}, [id]);
const hasItems = timeline ? timeline.length > 0 : false;
return {
events: timeline || [],
loading: hasItems ? false : loading,
error,
};
}