İçeriğe geç
react

React Concurrent Features: useTransition ve useDeferredValue

Tarık Tunç
React Concurrent Features: useTransition ve useDeferredValue

React Concurrent Features: useTransition ve useDeferredValue

React 18 ile gelen Concurrent Features, kullanıcı arayüzünün her zaman duyarlı kalmasını sağlayan devrim niteliğinde özelliklerdir. useTransition ve useDeferredValue hook'ları, ağır render işlemlerini arka plana atarak kullanıcı etkileşiminin kesintisiz devam etmesini mümkün kılar. Bu rehberde, bu iki hook'un çalışma mantığını, farklarını ve gerçek dünya kullanım senaryolarını adım adım inceleyeceğiz.

Concurrent Rendering Nedir?

Geleneksel React rendering'de (senkron mod), bir render işlemi başladığında tamamlanana kadar kesintiye uğratılamaz. Bu, büyük listelerin filtrelenmesi veya ağır hesaplamalar sırasında arayüzün "donmasına" yol açar. Concurrent rendering ile React, render işlemlerini önceliklendirerek acil güncellemeleri (input yazma, tıklama) düşük öncelikli güncellemelerden (liste filtreleme, ağır hesaplama) önce işler.

useTransition Hook

useTransition, bir state güncellemesini "düşük öncelikli" olarak işaretlemenizi sağlar. Bu sayede kullanıcı arayüzü meşgul olsa bile, acil güncellemeler (input değişiklikleri gibi) hemen işlenir.

import { useState, useTransition, useMemo } from 'react';

interface Product {
  id: string;
  name: string;
  category: string;
  price: number;
  rating: number;
}

// 10.000 ürünlük büyük bir listeyi filtreleyen component
function ProductSearch({ products }: { products: Product[] }) {
  const [query, setQuery] = useState('');
  const [category, setCategory] = useState('all');
  const [sortBy, setSortBy] = useState<'price' | 'rating'>('price');
  const [isPending, startTransition] = useTransition();

  // Bu state'ler transition içinde güncellenir (düşük öncelikli)
  const [deferredQuery, setDeferredQuery] = useState('');
  const [deferredCategory, setDeferredCategory] = useState('all');
  const [deferredSortBy, setDeferredSortBy] = useState<'price' | 'rating'>('price');

  const handleQueryChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    // Input güncellemesi acil — hemen görünür
    setQuery(e.target.value);

    // Filtreleme düşük öncelikli — arka planda işlenir
    startTransition(() => {
      setDeferredQuery(e.target.value);
    });
  };

  const handleCategoryChange = (newCategory: string) => {
    setCategory(newCategory);
    startTransition(() => {
      setDeferredCategory(newCategory);
    });
  };

  const filteredProducts = useMemo(() => {
    let result = products;

    if (deferredQuery) {
      result = result.filter((p) =>
        p.name.toLowerCase().includes(deferredQuery.toLowerCase())
      );
    }

    if (deferredCategory !== 'all') {
      result = result.filter((p) => p.category === deferredCategory);
    }

    result.sort((a, b) =>
      deferredSortBy === 'price' ? a.price - b.price : b.rating - a.rating
    );

    return result;
  }, [products, deferredQuery, deferredCategory, deferredSortBy]);

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={handleQueryChange}
        placeholder="Ürün ara..."
        className="border rounded px-3 py-2 w-full"
      />

      {isPending && (
        <div className="text-blue-500 text-sm mt-1">Sonuçlar güncelleniyor...</div>
      )}

      <div style={{ opacity: isPending ? 0.7 : 1, transition: 'opacity 0.2s' }}>
        <p>{filteredProducts.length} ürün bulundu</p>
        {filteredProducts.slice(0, 50).map((product) => (
          <div key={product.id} className="border-b py-2">
            <h3>{product.name}</h3>
            <span>{product.price} TL — ⭐ {product.rating}</span>
          </div>
        ))}
      </div>
    </div>
  );
}
İpucu: useTransition'dan dönen isPending boolean değerini kullanarak, arka planda işlem devam ederken kullanıcıya görsel geri bildirim verebilirsiniz. Örneğin listeyi soluklaştırma veya küçük bir spinner gösterme gibi.

useDeferredValue Hook

useDeferredValue, bir değerin "ertelenmiş" bir kopyasını oluşturur. Acil güncellemeler işlenirken, ertelenmiş değer eski halinde kalır ve React uygun olduğunda güncellenir. useTransition'dan farkı, state güncellemesini kontrol edemediğiniz durumlarda (props olarak gelen değerler gibi) kullanılmasıdır.

import { useState, useDeferredValue, useMemo } from 'react';

interface SearchResultsProps {
  query: string;
  items: string[];
}

function SearchResults({ query, items }: SearchResultsProps) {
  // query prop'u dışarıdan geliyor — useTransition kullanamayız
  // useDeferredValue ile ertelenmiş bir kopya oluşturuyoruz
  const deferredQuery = useDeferredValue(query);
  const isStale = query !== deferredQuery;

  const filteredItems = useMemo(() => {
    if (!deferredQuery) return items;

    // Ağır filtreleme simülasyonu
    return items.filter((item) => {
      // Her item için karmaşık eşleştirme
      const words = deferredQuery.toLowerCase().split(' ');
      return words.every((word) => item.toLowerCase().includes(word));
    });
  }, [items, deferredQuery]);

  return (
    <div style={{ opacity: isStale ? 0.6 : 1, transition: 'opacity 0.15s' }}>
      <p className="text-sm text-gray-500">
        {filteredItems.length} sonuç {isStale && '(güncelleniyor...)'}
      </p>
      <ul>
        {filteredItems.map((item, index) => (
          <li key={index} className="py-1 border-b">{item}</li>
        ))}
      </ul>
    </div>
  );
}

// Ana component
function SearchPage() {
  const [query, setQuery] = useState('');
  const allItems = useMemo(() =>
    Array.from({ length: 10000 }, (_, i) => `Ürün ${i + 1} - Kategori ${i % 10}`),
    []
  );

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Ara..."
      />
      <SearchResults query={query} items={allItems} />
    </div>
  );
}

useTransition vs useDeferredValue: Ne Zaman Hangisi?

Özellik useTransition useDeferredValue
Kullanım State güncellemesini siz kontrol ediyorsanız Props olarak gelen değerler için
isPending Var Manuel karşılaştırma gerekir
Kontrol Seviyesi Yüksek — hangi güncellemenin erteleneceğini seçersiniz Düşük — sadece değeri erteler
Yaygın Senaryo Tab değiştirme, filtreleme, navigasyon Arama sonuçları, büyük listeler

Gerçek Dünya Senaryosu: Tab Geçişleri

import { useState, useTransition } from 'react';

type Tab = 'posts' | 'comments' | 'photos';

function TabContainer() {
  const [activeTab, setActiveTab] = useState<Tab>('posts');
  const [isPending, startTransition] = useTransition();

  const handleTabChange = (tab: Tab) => {
    startTransition(() => {
      setActiveTab(tab);
    });
  };

  const tabs: Tab[] = ['posts', 'comments', 'photos'];

  return (
    <div>
      <nav className="flex gap-2 mb-4">
        {tabs.map((tab) => (
          <button
            key={tab}
            onClick={() => handleTabChange(tab)}
            className={`px-4 py-2 rounded ${
              activeTab === tab ? 'bg-blue-600 text-white' : 'bg-gray-100'
            }`}
          >
            {tab.charAt(0).toUpperCase() + tab.slice(1)}
          </button>
        ))}
      </nav>

      <div style={{ opacity: isPending ? 0.6 : 1 }}>
        {activeTab === 'posts' && <PostsTab />}
        {activeTab === 'comments' && <CommentsTab />}
        {activeTab === 'photos' && <PhotosTab />}
      </div>
    </div>
  );
}
Uyarı: useTransition ve useDeferredValue her durumda kullanılmamalıdır. Sadece gerçekten performans sorunu yaşadığınız ve kullanıcı arayüzünün donduğu durumlarda kullanın. Gereksiz kullanım kodunuzu karmaşıklaştırır ve debug etmeyi zorlaştırır.
Bilgi: Concurrent Features, React'in StrictMode'da componentleri iki kez render etmesinin bir nedenidir. Bu, concurrent rendering ile uyumlu olmayan yan etkileri tespit etmenize yardımcı olur.

Sonuç

React Concurrent Features, kullanıcı deneyimini bir üst seviyeye taşıyan güçlü araçlardır. useTransition ile state güncellemelerini önceliklendirerek, useDeferredValue ile ağır hesaplamaları erteleyerek uygulamanızın her zaman duyarlı kalmasını sağlayabilirsiniz. Ancak bu araçları bilinçli kullanmak önemlidir — önce performans sorununu tespit edin, sonra çözüm olarak Concurrent Features'ı değerlendirin.

Kaynaklar

İlgili Yazılar