İçeriğe geç
react

useRef Hook'u: DOM Erişimi ve Kalıcı Değerler

Tarık Tunç
useRef Hook'u: DOM Erişimi ve Kalıcı Değerler

useRef Hook'u: DOM Erişimi ve Kalıcı Değerler

React'in deklaratif yapısı çoğu durumda DOM ile doğrudan etkileşim ihtiyacını ortadan kaldırır. Ancak input'a odaklanma, scroll pozisyonu yönetimi veya üçüncü parti kütüphane entegrasyonu gibi durumlarda DOM erişimi kaçınılmazdır. useRef hook'u, DOM referansları tutmanın yanı sıra render'lar arasında kalıcı değerler saklamak için de kullanılır. Bu rehberde useRef'in tüm kullanım senaryolarını ve en iyi uygulamaları ele alacağız.

useRef'in Temeli

useRef, { current: initialValue } şeklinde bir ref objesi döndürür. Bu objenin iki temel özelliği vardır:

  • Render'lar arasında değerini korur (useState gibi)
  • Değeri değiştiğinde re-render tetiklemez (useState'ten farklı olarak)
import { useRef, useEffect, useState } from 'react';

function TextInput() {
  const inputRef = useRef(null);

  useEffect(() => {
    // Bileşen mount olduğunda input'a odaklan
    inputRef.current.focus();
  }, []);

  const handleClear = () => {
    inputRef.current.value = '';
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="Bir şey yazın..." />
      <button onClick={handleClear}>Temizle</button>
    </div>
  );
}

DOM Erişim Senaryoları

1. Scroll Yönetimi

function ChatMessages({ messages }) {
  const messagesEndRef = useRef(null);
  const containerRef = useRef(null);

  const scrollToBottom = () => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  };

  // Yeni mesaj geldiğinde en alta kaydır
  useEffect(() => {
    scrollToBottom();
  }, [messages]);

  const scrollToTop = () => {
    containerRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
  };

  return (
    <div ref={containerRef} className="chat-container" style={{ overflowY: 'auto', maxHeight: '400px' }}>
      {messages.map(msg => (
        <div key={msg.id} className="message">
          <strong>{msg.sender}:</strong> {msg.text}
        </div>
      ))}
      <div ref={messagesEndRef} />
      <button onClick={scrollToTop} className="scroll-top-btn">Yukarı</button>
    </div>
  );
}

2. Element Boyutu Ölçme

function ResizablePanel() {
  const panelRef = useRef(null);
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });

  useEffect(() => {
    const observer = new ResizeObserver(entries => {
      for (const entry of entries) {
        setDimensions({
          width: entry.contentRect.width,
          height: entry.contentRect.height
        });
      }
    });

    if (panelRef.current) {
      observer.observe(panelRef.current);
    }

    return () => observer.disconnect();
  }, []);

  return (
    <div ref={panelRef} className="resizable-panel">
      <p>Boyut: {dimensions.width.toFixed(0)}px x {dimensions.height.toFixed(0)}px</p>
    </div>
  );
}

Kalıcı Değer Olarak useRef

useRef yalnızca DOM erişimi için değil, render'lar arasında re-render tetiklemeden değer saklamak için de kullanılır:

function StopWatch() {
  const [elapsed, setElapsed] = useState(0);
  const [isRunning, setIsRunning] = useState(false);
  const intervalRef = useRef(null);      // interval ID'si
  const startTimeRef = useRef(null);     // başlangıç zamanı

  const start = () => {
    if (isRunning) return;
    setIsRunning(true);
    startTimeRef.current = Date.now() - elapsed;
    intervalRef.current = setInterval(() => {
      setElapsed(Date.now() - startTimeRef.current);
    }, 10);
  };

  const stop = () => {
    setIsRunning(false);
    clearInterval(intervalRef.current);
  };

  const reset = () => {
    stop();
    setElapsed(0);
  };

  // Cleanup
  useEffect(() => {
    return () => clearInterval(intervalRef.current);
  }, []);

  const formatTime = (ms) => {
    const minutes = Math.floor(ms / 60000);
    const seconds = Math.floor((ms % 60000) / 1000);
    const centiseconds = Math.floor((ms % 1000) / 10);
    return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}.${String(centiseconds).padStart(2, '0')}`;
  };

  return (
    <div className="stopwatch">
      <span className="time">{formatTime(elapsed)}</span>
      <button onClick={start} disabled={isRunning}>Başlat</button>
      <button onClick={stop} disabled={!isRunning}>Durdur</button>
      <button onClick={reset}>Sıfırla</button>
    </div>
  );
}

İpucu: Bir değerin değişmesi UI'da güncelleme gerektirmiyorsa, useState yerine useRef kullanın. Bu gereksiz re-render'ları önler. Timer ID'leri, önceki değerler, flag'ler ve API controller'ları useRef için ideal kullanım alanlarıdır.

Önceki Değeri Takip Etme (usePrevious Pattern)

function usePrevious(value) {
  const ref = useRef();

  useEffect(() => {
    ref.current = value;
  });

  return ref.current;
}

// Kullanım
function PriceTracker({ price }) {
  const previousPrice = usePrevious(price);

  const priceChange = previousPrice !== undefined
    ? price - previousPrice
    : 0;

  return (
    <div>
      <p>Güncel Fiyat: {price} TL</p>
      {previousPrice !== undefined && (
        <p style={{ color: priceChange > 0 ? 'green' : priceChange < 0 ? 'red' : 'gray' }}>
          {priceChange > 0 ? '+' : ''}{priceChange.toFixed(2)} TL
        </p>
      )}
    </div>
  );
}

forwardRef ile Ref Aktarma

import { forwardRef, useRef, useImperativeHandle } from 'react';

// forwardRef ile ref'i alt bileşene aktarma
const CustomInput = forwardRef(function CustomInput({ label, ...props }, ref) {
  return (
    <div className="form-group">
      <label>{label}</label>
      <input ref={ref} {...props} />
    </div>
  );
});

// useImperativeHandle ile özel API sunma
const VideoPlayer = forwardRef(function VideoPlayer({ src }, ref) {
  const videoRef = useRef(null);

  useImperativeHandle(ref, () => ({
    play: () => videoRef.current.play(),
    pause: () => videoRef.current.pause(),
    seekTo: (time) => { videoRef.current.currentTime = time; },
    getDuration: () => videoRef.current.duration
  }));

  return <video ref={videoRef} src={src} />;
});

// Kullanım
function MediaPage() {
  const playerRef = useRef(null);

  return (
    <div>
      <VideoPlayer ref={playerRef} src="/video.mp4" />
      <button onClick={() => playerRef.current.play()}>Oynat</button>
      <button onClick={() => playerRef.current.pause()}>Duraklat</button>
      <button onClick={() => playerRef.current.seekTo(0)}>Başa Sar</button>
    </div>
  );
}

Uyarı: Ref ile DOM'u doğrudan manipüle etmek (element ekleme/silme gibi) React'in kontrol ettiği DOM bölgelerinde çakışmalara neden olabilir. Ref'i yalnızca okuma amaçlı (focus, scroll, boyut ölçme) veya React'in yönetmediği DOM bölgelerinde kullanın.

Sonuç

useRef, React'te DOM erişimi ve render-bağımsız değer saklama için vazgeçilmez bir araçtır. DOM referansları, timer ID'leri, önceki değerlerin takibi ve üçüncü parti kütüphane entegrasyonları gibi birçok senaryoda kullanılır. forwardRef ve useImperativeHandle ile birlikte kullanıldığında, bileşenler arası DOM erişimini güvenli ve kontrollü bir şekilde sağlar. Ancak ref'i state yerine kullanma tuzağına düşmeyin; UI'da yansıması gereken değerler her zaman state olarak yönetilmelidir.

Kaynaklar

İlgili Yazılar