Supabase Realtime ile React'te Canlı Veri Takibi

Supabase Realtime ile React'te gerçek zamanlı veri nasıl takip edilir? postgres_changes, broadcast, channel yönetimi ve memory leak önleme rehberi.

Gerçek Zamanlı Güncelleme Neden Önemli?

Dashboard’da satış verisi güncellendi — ama kullanıcı sayfayı yenilemeden görmüyor. Chat mesajı geldi — karşı taraf anlamıyor. Stok düştü — ilgili kişi saatler sonra fark ediyor.

Supabase Realtime bu sorunları WebSocket üzerinden çözer.

Temel Kullanım: Tablo Dinleme

import { useEffect, useState } from 'react';
import { supabase } from '../lib/supabase';

function LiveOrders() {
  const [orders, setOrders] = useState<Order[]>([]);

  useEffect(() => {
    // Mevcut siparişleri çek
    supabase.from('orders').select('*').order('created_at', { ascending: false })
      .then(({ data }) => setOrders(data || []));

    // Gerçek zamanlı dinlemeye başla
    const channel = supabase
      .channel('orders-changes')
      .on(
        'postgres_changes',
        { event: 'INSERT', schema: 'public', table: 'orders' },
        (payload) => {
          setOrders(prev => [payload.new as Order, ...prev]);
        }
      )
      .on(
        'postgres_changes',
        { event: 'UPDATE', schema: 'public', table: 'orders' },
        (payload) => {
          setOrders(prev => prev.map(o => 
            o.id === payload.new.id ? payload.new as Order : o
          ));
        }
      )
      .subscribe();

    // Cleanup — memory leak önleme
    return () => {
      supabase.removeChannel(channel);
    };
  }, []);

  return (
    <ul>
      {orders.map(order => (
        <li key={order.id}>{order.customer} — ₺{order.total}</li>
      ))}
    </ul>
  );
}

Custom Hook: useRealtimeQuery

import { useEffect, useState, useCallback } from 'react';
import { supabase } from '../lib/supabase';
import { RealtimePostgresChangesPayload } from '@supabase/supabase-js';

export function useRealtimeQuery<T extends { id: string }>(
  table: string,
  query?: string
) {
  const [data, setData] = useState<T[]>([]);
  const [loading, setLoading] = useState(true);

  // İlk veriyi çek
  const fetchData = useCallback(async () => {
    const { data: rows } = await supabase
      .from(table)
      .select(query || '*')
      .order('created_at', { ascending: false });
    setData((rows as T[]) || []);
    setLoading(false);
  }, [table, query]);

  useEffect(() => {
    fetchData();

    const channel = supabase
      .channel(`${table}-realtime`)
      .on(
        'postgres_changes',
        { event: '*', schema: 'public', table },
        (payload: RealtimePostgresChangesPayload<T>) => {
          if (payload.eventType === 'INSERT') {
            setData(prev => [payload.new as T, ...prev]);
          } else if (payload.eventType === 'UPDATE') {
            setData(prev => prev.map(item => 
              item.id === (payload.new as T).id ? payload.new as T : item
            ));
          } else if (payload.eventType === 'DELETE') {
            setData(prev => prev.filter(item => 
              item.id !== (payload.old as T).id
            ));
          }
        }
      )
      .subscribe();

    return () => { supabase.removeChannel(channel); };
  }, [table, fetchData]);

  return { data, loading, refetch: fetchData };
}

// Kullanım
function Dashboard() {
  const { data: orders, loading } = useRealtimeQuery<Order>('orders');
  if (loading) return <Skeleton />;
  return <OrderTable data={orders} />;
}

RLS + Realtime Güvenliği

Realtime, RLS politikalarına tabidir. Kullanıcı sadece yetkili olduğu satırları alır:

-- Yalnızca kendi tenant'ının siparişlerini görsün
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

CREATE POLICY "tenant_orders" ON orders
  FOR SELECT
  USING (tenant_id = (auth.jwt() -> 'app_metadata' ->> 'tenant_id')::uuid);

⚠️ RLS olmadan tüm INSERT/UPDATE/DELETE eventleri tüm dinleyicilere gider!

Broadcast: Ephemeral Mesajlaşma

Database’e yazılmayan, anlık mesajlar:

// Cursor pozisyonu paylaşma (Google Docs gibi)
const channel = supabase.channel('room-1');

// Gönder
channel.send({
  type: 'broadcast',
  event: 'cursor',
  payload: { x: 100, y: 200, user: 'Ahmet' },
});

// Dinle
channel.on('broadcast', { event: 'cursor' }, ({ payload }) => {
  moveCursor(payload.x, payload.y, payload.user);
}).subscribe();

Dikkat Edilecekler

  1. Cleanup zorunlu: useEffect return’ünde removeChannel çağrın — yoksa memory leak.
  2. Duplicate subscription: Component re-render’da yeni channel oluşmaz — dependency array doğru olmalı.
  3. Büyük tablolar: Tüm değişiklikleri dinlemeyin, filtre kullanın.
  4. Offline durumu: WebSocket koparsa Supabase otomatik reconnect yapar.

İlgili Yazılar


Bu rehber Supabase Realtime Docs dokümantasyonuna dayanarak hazırlanmıştır.

📚 İlgili Yazılar