File size: 3,691 Bytes
5c98911 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | // Generic Realtime subscription hook for Supabase tables
// Fetches initial data then listens for INSERT/UPDATE/DELETE events
import { useEffect, useState, useCallback, useRef } from 'react';
import { supabase } from '../lib/supabase';
import type { RealtimeChannel } from '@supabase/supabase-js';
type Row = { [key: string]: unknown };
interface UseRealtimeQueryOptions<T> {
table: string;
/** Supabase filter string, e.g. 'user_id=eq.abc123' */
filter?: string;
/** Column to order by (default: created_at desc) */
orderBy?: string;
ascending?: boolean;
limit?: number;
/** Map a raw DB row to the UI type */
mapRow: (row: Row) => T;
/** Unique key extractor (default: row.id) */
getKey?: (item: T) => string;
enabled?: boolean;
}
interface UseRealtimeQueryResult<T> {
data: T[];
loading: boolean;
error: string | null;
refetch: () => Promise<void>;
}
export function useRealtimeQuery<T>(
options: UseRealtimeQueryOptions<T>,
): UseRealtimeQueryResult<T> {
const {
table,
filter,
orderBy = 'created_at',
ascending = false,
limit,
mapRow,
getKey = (item: T) => (item as Row).id as string,
enabled = true,
} = options;
const [data, setData] = useState<T[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const channelRef = useRef<RealtimeChannel | null>(null);
const fetchData = useCallback(async () => {
if (!enabled) return;
setLoading(true);
setError(null);
let query = supabase.from(table).select('*').order(orderBy, { ascending });
if (limit) query = query.limit(limit);
// Apply simple eq filters from the filter string
if (filter) {
const parts = filter.split('=eq.');
if (parts.length === 2) {
query = query.eq(parts[0], parts[1]);
}
}
const { data: rows, error: err } = await query;
if (err) {
setError(err.message);
setLoading(false);
return;
}
setData((rows ?? []).map(mapRow));
setLoading(false);
}, [table, filter, orderBy, ascending, limit, mapRow, enabled]);
useEffect(() => {
if (!enabled) return;
fetchData();
// Realtime subscription
const channelName = `realtime-${table}-${filter ?? 'all'}`;
const channel = supabase
.channel(channelName)
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table,
...(filter ? { filter } : {}),
},
(payload) => {
const eventType = payload.eventType;
if (eventType === 'INSERT') {
const newItem = mapRow(payload.new as Row);
setData((prev) =>
ascending ? [...prev, newItem] : [newItem, ...prev],
);
} else if (eventType === 'UPDATE') {
const updatedItem = mapRow(payload.new as Row);
const key = getKey(updatedItem);
setData((prev) =>
prev.map((item) => (getKey(item) === key ? updatedItem : item)),
);
} else if (eventType === 'DELETE') {
const oldId = (payload.old as Row).id as string;
if (oldId) {
setData((prev) =>
prev.filter((item) => getKey(item) !== oldId),
);
}
}
},
)
.subscribe();
channelRef.current = channel;
return () => {
if (channelRef.current) {
supabase.removeChannel(channelRef.current);
channelRef.current = null;
}
};
}, [table, filter, enabled, ascending, fetchData, mapRow, getKey]);
return { data, loading, error, refetch: fetchData };
}
|