Skip to content

React Hooks ve Custom Hooks

Paylaşılan React hook'ları: useLang, useVideoTelemetry, useChatNotifications ve kullanım örnekleri.

Achidemy projesinde paylaşılan React hook’ları app/hooks/ klasöründe bulunur. Bu hook’lar route ve component’lerde tekrar kullanılır ve tutarlı davranış sağlar.

Dosya: app/hooks/useLang.ts

Amaç: Mevcut dili ve dil destekli URL oluşturma fonksiyonlarını sağlar.

import { useLang } from '~/hooks/useLang';
function MyComponent() {
const { lang, getLocalizedPath, getPath } = useLang();
// Mevcut dil: 'tr', 'en', 'de', 'es', vb.
console.log(lang); // 'tr'
// Dil prefix'li path oluştur
const localizedPath = getLocalizedPath('/courses'); // '/tr/courses'
// Admin/API route'ları için path'i olduğu gibi döndür
const adminPath = getPath('/admin/users'); // '/admin/users'
}
DeğerTipAçıklama
langSupportedLanguageMevcut dil kodu ('tr', 'en', 'de', 'es', 'fr', 'ja')
getLocalizedPath(path: string) => stringPath’e mevcut dil kodunu ekler veya değiştirir
getPath(path: string) => stringAdmin/API route’ları için path’i olduğu gibi döndürür, diğerleri için dil ekler
// Dil değiştirme linki
const { lang, getLocalizedPath } = useLang();
const englishPath = getLocalizedPath('/courses'); // Mevcut dil 'tr' ise '/en/courses'
// Navigasyon
<Link to={getLocalizedPath('/instructor/dashboard')}>
Dashboard
</Link>

Dosya: app/hooks/useVideoTelemetry.ts

Amaç: Video izlenme süresini kayıpsız tampon (lossless buffer) ile takip eder ve backend’e 15 saniyelik heartbeat blokları halinde gönderir. Sunucu 200 OK dönmeden client tamponundan düşmez.

import { useVideoTelemetry } from "~/hooks/useVideoTelemetry";
export function CoursePlayerTelemetry({ courseId, lessonId, isPlaying }: { courseId?: string; lessonId?: string; isPlaying: boolean }) {
useVideoTelemetry({ courseId, lessonId, isPlaying });
return null;
}
ParametreTipAçıklama
courseIdstring | undefinedKurs ID
lessonIdstring | undefinedDers ID
isPlayingbooleanVideo oynuyor mu? (learn.$slug içinde “video time ilerliyor mu?” ile türetilir)
  • Endpoint: POST /api/video-telemetry
  • Blok boyutu: 15 saniye (heartbeat)
  • Anti-cheat: tek istekte durationSeconds <= 25

Dosya: app/hooks/useLearningTracker.ts

Amaç: (Legacy) Dakika bazlı izlenme raporu ve /api/update-progress üzerinden heartbeat. Yeni sistemde useVideoTelemetry kullanılmalıdır.

import { useLearningTracker } from '~/hooks/useLearningTracker';
function VideoPlayer({ video, courseId, lessonId, instructorId, isSubscriber }) {
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const { totalMinutes, totalSeconds } = useLearningTracker({
isPlaying,
courseId,
lessonId,
instructorId,
isSubscriber,
currentTimeSeconds: currentTime,
onMinuteCompleted: (minutes) => {
console.log(`${minutes} dakika izlendi`);
}
});
return (
<div>
<video onPlay={() => setIsPlaying(true)} onPause={() => setIsPlaying(false)} />
<p>Toplam izlenen: {totalMinutes} dakika</p>
</div>
);
}
ParametreTipAçıklama
isPlayingbooleanVideo oynatılıyor mu?
courseIdstringKurs ID
lessonIdstringDers ID
instructorIdstringEğitmen ID
isSubscriberbooleanAbonelik ile mi izleniyor?
currentTimeSecondsnumberVideonun o anki konumu (saniye)
onMinuteCompleted(totalMinutes: number) => voidHer dakika tamamlandığında çağrılır (opsiyonel)
DeğerTipAçıklama
totalMinutesnumberToplam izlenen dakika (tam sayı)
totalSecondsnumberToplam izlenen saniye
  1. İzleme Takibi: Video oynatıldığında süre biriktirilir
  2. Dakika Tamamlama: Her dakika tamamlandığında:
    • GraphQL mutation: recordLearningActivity(minutes) çağrılır
    • Streak ve günlük aktivite kaydı yapılır
  3. Heartbeat: (Legacy) Her ~30 saniyede bir /api/update-progress endpoint’ine ilerleme gönderilir
    • secondsWatched, courseId, lessonId, instructorId, isSubscription bilgileri gönderilir

GraphQL Mutation:

mutation RecordActivity($minutes: Int!) {
recordLearningActivity(minutes: $minutes)
}

API Endpoint (Legacy): /api/update-progress

POST /api/update-progress
{
courseId: string;
lessonId: string;
instructorId: string;
secondsWatched: number;
isSubscription: boolean;
}

Dosya: app/hooks/useChatNotifications.ts

Amaç: Kullanıcının okunmamış mesaj sayısını takip eder ve WebSocket ile gerçek zamanlı güncelleme sağlar.

import { useChatNotifications } from '~/hooks/useChatNotifications';
function Navbar() {
const { user } = useAuth();
const { unreadCount, refetch } = useChatNotifications(user?.id);
return (
<nav>
<Link to="/messages">
Mesajlar
{unreadCount > 0 && (
<Badge>{unreadCount}</Badge>
)}
</Link>
</nav>
);
}
ParametreTipAçıklama
userIdstring | undefinedKullanıcı ID (undefined ise hook çalışmaz)
DeğerTipAçıklama
unreadCountnumberOkunmamış mesaj sayısı
refetch() => Promise<void>Manuel olarak sayıyı yeniden çekme fonksiyonu
  1. İlk Yükleme: Component mount olduğunda okunmamış mesaj sayısı çekilir
  2. WebSocket Bağlantısı: /api/chat?userId={userId}&convId={convId} üzerinden WebSocket bağlantısı kurulur
  3. Gerçek Zamanlı Güncelleme:
    • NEW_MESSAGE event’i geldiğinde sayı artırılır
    • UPDATE_BADGE event’i geldiğinde sayı güncellenir
  4. Sayfa Görünürlüğü: Sayfa görünür olduğunda sayı yeniden çekilir
  5. Periyodik Güncelleme: Her 30 saniyede bir sayı yeniden çekilir (WebSocket bağlantısı yoksa)
EventAçıklama
NEW_MESSAGEYeni mesaj geldiğinde
UPDATE_BADGEBadge sayısı güncellendiğinde

Loader verisi kullanan route bileşenlerinde skeleton veya loading göstermek için erken return kullanılıyorsa, React’in hook kurallarına uyulmalıdır. Aksi halde sayfa geçişlerinde “Rendered fewer hooks than expected” (React #300) hatası oluşur.

  • Tüm hook’lar her zaman aynı sırada çağrılmalı. Hiçbir if (loading) return <Skeleton /> veya if (!data) return null ifadesi, useLoaderData, useState, useEffect vb. hook’lardan önce olmamalı.
  • Doğru sıra: Önce tüm hook’lar (useNavigation, useLoaderData, useNavigate, useRevalidator, useState’ler, useTranslation, useLang, useEffect vb.), sonra isteğe bağlı hesaplamalar (loader verisi ile, güvenli erişimle), en sonda loading/veri yok return’leri ve asıl JSX.
export default function CourseDetails() {
const navigation = useNavigation();
const loaderData = useLoaderData<typeof loader>();
const course = loaderData?.course;
const curriculum = loaderData?.curriculum ?? [];
const [previewVideo, setPreviewVideo] = useState(null);
const navigate = useNavigate();
const { revalidate } = useRevalidator();
const { t } = useTranslation();
const { getLocalizedPath } = useLang();
useEffect(() => { /* ... */ }, [course?.id]);
// Hesaplamalar (hook'lardan sonra, güvenli erişim)
const totalLessons = (curriculum ?? []).reduce(
(acc, section) => acc + (section?.lessons ?? []).length,
0
);
if (navigation.state === "loading") return <CoursePageSkeleton />;
if (!course) return null;
return (/* asıl JSX */);
}

Loader verisi loaderData?.field ve ?? [] ile alınır; hesaplamalar hook’lardan sonra yapılır; loading ve !course return’leri en sonda yer alır.


  • app/hooks/useLang.ts — Dil yönetimi hook’u
  • app/hooks/useLearningTracker.ts — Öğrenme takibi hook’u
  • app/hooks/useChatNotifications.ts — Mesaj bildirimleri hook’u
  • app/lib/i18n-utils.ts — Dil yardımcı fonksiyonları
  • app/lib/streak.ts — Streak yönetimi (useLearningTracker tarafından kullanılır)
  • workers/chat.ts — WebSocket chat (useChatNotifications tarafından kullanılır)