İçeriğe geç
Sedat Demir
Geri dön

React Activity Bileşeni: State Kaybetmeden UI Parçalarını Gizle ve Göster

React Activity Bileşeni: State Kaybetmeden UI Parçalarını Gizle ve Göster

React geliştirirken en sık karşılaştığımız senaryolardan biri, bir bileşeni koşullu olarak render etmektir. Bir tab değiştiğinde, bir modal kapandığında veya bir sidebar gizlendiğinde genellikle şu klasik kalıbı kullanırız:

{isVisible && <MyComponent />}

Bu yaklaşım işe yarar — ama büyük bir bedeli vardır: bileşen unmount olduğunda tüm internal state'i kaybolur. Kullanıcı bir formu yarıda bırakıp başka bir sekmeye geçtiğinde, geri döndüğünde her şeyin sıfırlandığını görmek son derece sinir bozucudur. İşte React ekibinin bu soruna yanıtı: <Activity> bileşeni.

Sorunun Kökeni: Koşullu Render ve State Kaybı

Klasik bir senaryo üzerinden gidelim. Diyelim ki bir tab yapınız var:

import { useState } from 'react';

function TabContainer() {
  const [activeTab, setActiveTab] = useState('editor');

  return (
    <div>
      <nav>
        <button onClick={() => setActiveTab('editor')}>Editör</button>
        <button onClick={() => setActiveTab('preview')}>Önizleme</button>
        <button onClick={() => setActiveTab('settings')}>Ayarlar</button>
      </nav>

      {activeTab === 'editor' && <TextEditor />}
      {activeTab === 'preview' && <Preview />}
      {activeTab === 'settings' && <Settings />}
    </div>
  );
}

function TextEditor() {
  const [content, setContent] = useState('');
  const [cursorPosition, setCursorPosition] = useState(0);

  return (
    <textarea
      value={content}
      onChange={(e) => {
        setContent(e.target.value);
        setCursorPosition(e.target.selectionStart);
      }}
      placeholder="Bir şeyler yazın..."
    />
  );
}

Kullanıcı editörde uzun bir metin yazar, "Önizleme" sekmesine geçer, ardından "Editör"e geri döner. Ne olur? Metin tamamen kaybolur. Çünkü TextEditor bileşeni unmount edilmiş ve tekrar mount edilmiştir.

Geleneksel Çözümler ve Dezavantajları

Bu sorunu çözmek için geliştiriciler genellikle şu yollara başvurur:

  1. State'i yukarı taşımak (lifting state up): Çalışır ama prop drilling yaratır ve üst bileşeni gereksiz yere şişirir.
  2. Global state yönetimi (Redux, Zustand vb.): Basit bir UI state'i için fazla mühendisliktir.
  3. CSS ile gizleme (display: none): State korunur ama tüm bileşenler her zaman render edilir, React yaşam döngüsü yönetimi kaybolur ve erişilebilirlik sorunları oluşur.
  4. localStorage/sessionStorage: Senkronizasyon karmaşıklığı ekler.

Hiçbiri gerçek anlamda "temiz" bir çözüm değildir. İşte tam bu noktada <Activity> devreye giriyor.

Activity Bileşeni Nedir?

<Activity> (daha önce <Offscreen> olarak bilinen), React ekibinin uzun süredir üzerinde çalıştığı deneysel bir API'dir. React 19 ile birlikte react paketinde erişilebilir hale gelmiştir — ancak hâlâ unstable (kararsız) olarak işaretlenmiştir.

Temel konsept son derece basittir:

Bir bileşeni görsel olarak gizle, ama React ağacında tutmaya devam et. Böylece state korunsun.

<Activity> bileşeni bir mode prop'u alır:

import { unstable_Activity as Activity } from 'react';

function TabContainer() {
  const [activeTab, setActiveTab] = useState('editor');

  return (
    <div>
      <nav>
        <button onClick={() => setActiveTab('editor')}>Editör</button>
        <button onClick={() => setActiveTab('preview')}>Önizleme</button>
        <button onClick={() => setActiveTab('settings')}>Ayarlar</button>
      </nav>

      <Activity mode={activeTab === 'editor' ? 'visible' : 'hidden'}>
        <TextEditor />
      </Activity>
      <Activity mode={activeTab === 'preview' ? 'visible' : 'hidden'}>
        <Preview />
      </Activity>
      <Activity mode={activeTab === 'settings' ? 'visible' : 'hidden'}>
        <Settings />
      </Activity>
    </div>
  );
}

Bu kadar. TextEditor bileşenindeki hiçbir state'i yukarı taşımanıza, global store'a almanıza veya CSS hack'leri yapmanıza gerek yok. Kullanıcı sekmeler arasında geçiş yaptığında her şey olduğu gibi kalır.

Nasıl Çalışır?

<Activity> bileşeninin iç mekanizması birkaç önemli davranış sergiler:

1. DOM'da Kalır, Görünmez Olur

mode="hidden" olduğunda React, sarmalanan bileşenlerin DOM düğümlerine display: none style'ı uygular. Bu, bileşenlerin aslında DOM'da var olduğu ancak görsel olarak gizlendiği anlamına gelir.

2. Effect'ler Temizlenir ve Yeniden Çalışır

Bu, display: none CSS hack'inden en önemli farkıdır. <Activity> hidden moduna geçtiğinde:

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const connection = connectToRoom(roomId);
    connection.on('message', (msg) => {
      setMessages((prev) => [...prev, msg]);
    });

    // Hidden moduna geçince bu cleanup çalışır
    return () => {
      connection.disconnect();
    };
  }, [roomId]);

  return (
    <ul>
      {messages.map((msg, i) => (
        <li key={i}>{msg.text}</li>
      ))}
    </ul>
  );
}

Bu davranış kritik öneme sahiptir: WebSocket bağlantıları, timer'lar, event listener'lar gibi yan etkiler hidden modda gereksiz yere çalışmaz. State (messages dizisi) korunur, ama aktif bağlantı düzgünce kapatılır.

3. Ref'ler Korunur

DOM ref'leri de korunur. Bileşen tekrar visible olduğunda aynı DOM düğümlerine işaret etmeye devam ederler.

4. Düşük Öncelikli Render

Hidden modundaki bileşenler düşük öncelikle render edilir. Bu, React'in görünür içeriği önceliklendirmesine olanak tanır ve algılanan performansı artırır.

Gerçek Dünya Kullanım Senaryoları

Çok Adımlı Formlar (Wizard)

import { unstable_Activity as Activity } from 'react';
import { useState } from 'react';

function RegistrationWizard() {
  const [step, setStep] = useState(1);

  return (
    <div className="wizard">
      <div className="steps-indicator">
        <span className={step >= 1 ? 'active' : ''}>Kişisel Bilgiler</span>
        <span className={step >= 2 ? 'active' : ''}>Adres</span>
        <span className={step >= 3 ? 'active' : ''}>Onay</span>
      </div>

      <Activity mode={step === 1 ? 'visible' : 'hidden'}>
        <PersonalInfoStep onNext={() => setStep(2)} />
      </Activity>

      <Activity mode={step === 2 ? 'visible' : 'hidden'}>
        <AddressStep
          onBack={() => setStep(1)}
          onNext={() => setStep(3)}
        />
      </Activity>

      <Activity mode={step === 3 ? 'visible' : 'hidden'}>
        <ConfirmationStep onBack={() => setStep(2)} />
      </Activity>
    </div>
  );
}

function PersonalInfoStep({ onNext }) {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  return (
    <div>
      <h2>Kişisel Bilgiler</h2>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Adınız"
      />
      <input
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="E-posta"
      />
      <button onClick={onNext}>İleri</button>
    </div>
  );
}

Kullanıcı 2. adıma geçip geri döndüğünde, adını ve e-postasını tekrar yazmak zorunda kalmaz. Bu, form terk oranını düşüren küçük ama kritik bir UX iyileştirmesidir.

Ön Yükleme (Pre-rendering)

<Activity> bileşeninin bir diğer güçlü kullanım alanı, kullanıcının henüz görmediği içeriği arka planda hazırlamaktır:

function ProductPage({ productId }) {
  const [showReviews, setShowReviews] = useState(false);

  return (
    <div>
      <ProductDetails id={productId} />

      <button onClick={() => setShowReviews(true)}>
        Yorumları Göster
      </button>

      {/* Yorumlar henüz görünmese bile arka planda hazırlanır */}
      <Activity mode={showReviews ? 'visible' : 'hidden'}>
        <ReviewsList productId={productId} />
      </Activity>
    </div>
  );
}

ReviewsList bileşeni sayfa yüklendiğinde düşük öncelikle render edilir. Kullanıcı "Yorumları Göster" butonuna tıkladığında, içerik zaten hazır olduğu için anında görünür.

Router Geçişlerinde State Koruma

SPA uygulamalarında sayfa geçişlerinde state kaybı en büyük acı noktalarından biridir. Activity bileşeni bu problemi de çözer:

function AppRouter() {
  const [currentRoute, setCurrentRoute] = useState('/home');

  return (
    <div>
      <Navigation onNavigate={setCurrentRoute} />

      <Activity mode={currentRoute === '/home' ? 'visible' : 'hidden'}>
        <HomePage />
      </Activity>

      <Activity mode={currentRoute === '/dashboard' ? 'visible' : 'hidden'}>
        <Dashboard />
      </Activity>

      <Activity mode={currentRoute === '/profile' ? 'visible' : 'hidden'}>
        <ProfilePage />
      </Activity>
    </div>
  );
}

Dashboard'daki filtreler, scroll pozisyonu, açık/kapalı paneller — her şey olduğu gibi kalır.

Activity vs CSS display: none — Fark Ne?

İlk bakışta "CSS ile de gizleyebiliriz" diye düşünebilirsiniz. Farkları tablo halinde inceleyelim:

Özellik CSS display: none <Activity mode="hidden">
State korunması ✅ Evet ✅ Evet
Effect cleanup ❌ Çalışmaz ✅ Çalışır
Render önceliklendirme ❌ Yok ✅ Düşük öncelik
Erişilebilirlik ❌ Sorunlu olabilir ✅ React tarafından yönetilir
Memory yönetimi ❌ Tüm effect'ler aktif kalır ✅ Gereksiz kaynaklar temizlenir
React ekosistemi entegrasyonu ❌ Yok ✅ Suspense, Transitions ile uyumlu

En kritik fark effect yönetimidir. CSS ile gizlediğinizde interval'lar çalışmaya devam eder, WebSocket bağlantıları açık kalır, animasyon frame'leri döner. <Activity> ise bunları düzgünce temizler.

Dikkat Edilmesi Gerekenler

1. Henüz Kararsız (Unstable) API

// Doğru import
import { unstable_Activity as Activity } from 'react';

unstable_ prefix'i, API'nin gelecekte değişebileceği anlamına gelir. Production uygulamalarında kullanırken bu riski göz önünde bulundurun.

2. Bellek Kullanımı

Gizli bileşenler DOM'da ve React ağacında kalmaya devam ettiğinden, çok sayıda ağır bileşeni aynı anda hidden modda tutmak bellek tüketimini artırabilir. Her şeyi Activity içine sarmak yerine, gerçekten state koruması gereken bileşenler için kullanın.

3. Effect'lerinizi Doğru Yazın

Activity'nin effect cleanup mekanizmasından tam faydalanabilmek için effect'lerinizin cleanup fonksiyonlarını düzgün yazmanız şarttır:

// ✅ Doğru: Cleanup fonksiyonu var
useEffect(() => {
  const timer = setInterval(() => {
    fetchNotifications();
  }, 30000);

  return () => clearInterval(timer);
}, []);

// ❌ Yanlış: Cleanup yok, timer hidden modda da çalışır
useEffect(() => {
  setInterval(() => {
    fetchNotifications();
  }, 30000);
}, []);

4. Suspense ile Birlikte Kullanım

<Activity> bileşeni Suspense ile sorunsuz çalışır. Hidden moddaki bir bileşen veri yüklüyorsa, bu işlem düşük öncelikle gerçekleşir ve visible içeriği bloke etmez.

<Activity mode={activeTab === 'analytics' ? 'visible' : 'hidden'}>
  <Suspense fallback={<Skeleton />}>
    <AnalyticsDashboard />
  </Suspense>
</Activity>

Custom Hook ile Kullanımı Kolaylaştırma

Tekrar eden kalıpları bir custom hook ile sarmalayabilirsiniz:

import { unstable_Activity as Activity } from 'react';
import { useState, useCallback } from 'react';

function useTabManager(tabs, defaultTab) {
  const [activeTab, setActiveTab] = useState(defaultTab);

  const TabPanel = useCallback(({ name, children }) => (
    <Activity mode={activeTab === name ? 'visible' : 'hidden'}>
      {children}
    </Activity>
  ), [activeTab]);

  return { activeTab, setActiveTab, TabPanel };
}

// Kullanım
function App() {
  const { activeTab, setActiveTab, TabPanel } = useTabManager(
    ['code', 'preview', 'console'],
    'code'
  );

  return (
    <div>
      <div className="tab-bar">
        {['code', 'preview', 'console'].map((tab) => (
          <button
            key={tab}
            className={activeTab === tab ? 'active' : ''}
            onClick={() => setActiveTab(tab)}
          >
            {tab}
          </button>
        ))}
      </div>

      <TabPanel name="code">
        <CodeEditor />
      </TabPanel>
      <TabPanel name="preview">
        <LivePreview />
      </TabPanel>
      <TabPanel name="console">
        <ConsoleOutput />
      </TabPanel>
    </div>
  );
}

Gelecek Perspektifi

React ekibi, <Activity> bileşenini daha geniş bir vizyonun parçası olarak konumlandırıyor. İlerleyen sürümlerde:

Sonuç

<Activity> bileşeni, React'te uzun süredir var olan "bileşen gizlendiğinde state kaybolur" problemine zarif ve framework-native bir çözüm sunuyor. CSS hack'lerine, gereksiz global state'e veya state lifting karmaşıklığına gerek kalmadan, bileşenlerinizi güvenle gizleyip gösterebilirsiniz.

Özetlemek gerekirse:

Bu API stabil hale geldiğinde, React uygulamalarında kullanıcı deneyimini iyileştirmenin standart yollarından biri haline gelecektir. Şimdiden deneyip alışmak, ilerideki geçişinizi kolaylaştıracaktır.


Share this post on:

Sonraki Yazı
React ViewTransition Bileşeni ile Native Sayfa Geçiş Animasyonları