Merge pull request #14 from purrgrammer/claude/stabilization-hooks-EeWQZ

feat: add dependency stabilization hooks
This commit is contained in:
Alejandro
2025-12-22 13:10:51 +01:00
committed by GitHub
4 changed files with 75 additions and 20 deletions

View File

@@ -1,8 +1,9 @@
import { useState, useEffect, useMemo } from "react";
import { useState, useEffect } from "react";
import pool from "@/services/relay-pool";
import type { NostrEvent, Filter } from "nostr-tools";
import { useEventStore, useObservableMemo } from "applesauce-react/hooks";
import { isNostrEvent } from "@/lib/type-guards";
import { useStableValue, useStableArray } from "./useStable";
interface UseLiveTimelineOptions {
limit?: number;
@@ -38,12 +39,9 @@ export function useLiveTimeline(
const [error, setError] = useState<Error | null>(null);
const [eoseReceived, setEoseReceived] = useState(false);
// Stabilize filters and relays for dependency array
// Using JSON.stringify and .join() for deep comparison - this is intentional
// eslint-disable-next-line react-hooks/exhaustive-deps
const stableFilters = useMemo(() => filters, [JSON.stringify(filters)]);
// eslint-disable-next-line react-hooks/exhaustive-deps
const stableRelays = useMemo(() => relays, [relays.join(",")]);
// Stabilize filters and relays to prevent unnecessary re-renders
const stableFilters = useStableValue(filters);
const stableRelays = useStableArray(relays);
// 1. Subscription Effect - Fetch data and feed EventStore
useEffect(() => {

View File

@@ -3,6 +3,7 @@ import pool from "@/services/relay-pool";
import type { NostrEvent, Filter } from "nostr-tools";
import { useEventStore } from "applesauce-react/hooks";
import { isNostrEvent } from "@/lib/type-guards";
import { useStableValue, useStableArray } from "./useStable";
interface UseReqTimelineOptions {
limit?: number;
@@ -47,12 +48,9 @@ export function useReqTimeline(
);
}, [eventsMap]);
// Stabilize filters and relays for dependency array
// Using JSON.stringify and .join() for deep comparison - this is intentional
// eslint-disable-next-line react-hooks/exhaustive-deps
const stableFilters = useMemo(() => filters, [JSON.stringify(filters)]);
// eslint-disable-next-line react-hooks/exhaustive-deps
const stableRelays = useMemo(() => relays, [relays.join(",")]);
// Stabilize filters and relays to prevent unnecessary re-renders
const stableFilters = useStableValue(filters);
const stableRelays = useStableArray(relays);
useEffect(() => {
if (relays.length === 0) {

61
src/hooks/useStable.ts Normal file
View File

@@ -0,0 +1,61 @@
import { useMemo } from "react";
/**
* Stabilize a value for use in dependency arrays
*
* React's useEffect/useMemo compare dependencies by reference.
* For objects/arrays that are recreated each render but have the same content,
* this causes unnecessary re-runs. This hook memoizes the value based on
* a serialized representation.
*
* @param value - The value to stabilize
* @param serialize - Optional custom serializer (defaults to JSON.stringify)
* @returns The memoized value
*
* @example
* ```typescript
* // Instead of: useMemo(() => filters, [JSON.stringify(filters)])
* const stableFilters = useStableValue(filters);
* ```
*/
export function useStableValue<T>(
value: T,
serialize?: (v: T) => string
): T {
const serialized = serialize?.(value) ?? JSON.stringify(value);
// eslint-disable-next-line react-hooks/exhaustive-deps
return useMemo(() => value, [serialized]);
}
/**
* Stabilize a string array for use in dependency arrays
*
* Uses JSON.stringify for safe serialization (handles arrays with commas in elements).
*
* @param arr - The array to stabilize
* @returns The memoized array
*
* @example
* ```typescript
* // Instead of: useMemo(() => relays, [JSON.stringify(relays)])
* const stableRelays = useStableArray(relays);
* ```
*/
export function useStableArray<T extends string>(arr: T[]): T[] {
// eslint-disable-next-line react-hooks/exhaustive-deps
return useMemo(() => arr, [JSON.stringify(arr)]);
}
/**
* Stabilize a Nostr filter or array of filters
*
* Specialized stabilizer for Nostr filters which are commonly
* recreated on each render.
*
* @param filters - Single filter or array of filters
* @returns The memoized filter(s)
*/
export function useStableFilters<T>(filters: T): T {
// eslint-disable-next-line react-hooks/exhaustive-deps
return useMemo(() => filters, [JSON.stringify(filters)]);
}

View File

@@ -1,9 +1,10 @@
import { useState, useEffect, useMemo } from "react";
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;
@@ -35,12 +36,9 @@ export function useTimeline(
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
// Stabilize filters and relays for dependency array
// Using JSON.stringify and .join() for deep comparison - this is intentional
// eslint-disable-next-line react-hooks/exhaustive-deps
const stableFilters = useMemo(() => filters, [JSON.stringify(filters)]);
// eslint-disable-next-line react-hooks/exhaustive-deps
const stableRelays = useMemo(() => relays, [relays.join(",")]);
// Stabilize filters and relays to prevent unnecessary re-renders
const stableFilters = useStableValue(filters);
const stableRelays = useStableArray(relays);
// Load events into store
useEffect(() => {