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:
- State'i yukarı taşımak (lifting state up): Çalışır ama prop drilling yaratır ve üst bileşeni gereksiz yere şişirir.
- Global state yönetimi (Redux, Zustand vb.): Basit bir UI state'i için fazla mühendisliktir.
- 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. - 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:
"visible": Bileşen normal şekilde görünür ve etkileşime açıktır."hidden": Bileşen DOM'dadisplay: noneile gizlenir, ancak React state'i tamamen korunur.
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:
- Tüm cleanup fonksiyonları (
useEffectreturn'leri) çalıştırılır. - Bileşen tekrar visible olduğunda effect'ler yeniden tetiklenir.
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:
- React Router ve diğer routing çözümleriyle native entegrasyon bekleniyor.
- View Transitions API ile birlikte çalışarak sayfa geçişlerinde animasyonlu state koruma mümkün olabilecek.
- Server Components ile entegrasyonu derinleşecek.
- API stabil hale geldiğinde
unstable_prefix'i kaldırılacak.
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:
- Ne yapar? UI parçalarını state'lerini koruyarak gizler/gösterir.
- Nasıl çalışır? DOM'da
display: noneuygular ama effect'leri düzgünce yönetir. - Ne zaman kullanmalı? Tab yapıları, wizard formlar, route geçişleri, ön yükleme senaryolarında.
- Ne zaman kullanmamalı? Basit koşullu render yeterli olduğunda veya state koruması gerekmediğinde.
- Dikkat: Henüz
unstable_prefix'iyle sunuluyor; production'da bilinçli kullanın.
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.