| |
| |
|
|
| 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; |
| |
| filter?: string; |
| |
| orderBy?: string; |
| ascending?: boolean; |
| limit?: number; |
| |
| mapRow: (row: Row) => T; |
| |
| 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); |
|
|
| |
| 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(); |
|
|
| |
| 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 }; |
| } |
|
|