İçeriğe geç
react

React Fragment ve Portal Kullanımı

Tarık Tunç
React Fragment ve Portal Kullanımı

React Fragment ve Portal Kullanımı

React'te bileşen yapısını düzenlerken sıkça karşılaşılan iki önemli kavram Fragment ve Portal'dır. Fragment, gereksiz DOM düğümlerinden kaçınmanızı sağlarken, Portal bileşenleri DOM hiyerarşisinin dışına render etmenize olanak tanır. Bu yazıda her iki kavramı derinlemesine inceleyeceğiz.

React Fragment Nedir?

React bileşenleri tek bir kök eleman döndürmelidir. Fragment, ekstra bir DOM düğümü oluşturmadan birden fazla elemanı gruplamanızı sağlar. Bu, gereksiz <div> sarmalayıcılardan kurtulmanın yoludur.

import { Fragment } from 'react';

// Sorun: Gereksiz div wrapper
function TableRow() {
  return (
    <div>  {/* Bu div table yapısını bozar! */}
      <td>İsim</td>
      <td>Yaş</td>
    </div>
  );
}

// Çözüm: Fragment kullanımı
function TableRow() {
  return (
    <Fragment>
      <td>İsim</td>
      <td>Yaş</td>
    </Fragment>
  );
}

// Kısa sözdizimi
function TableRow() {
  return (
    <>
      <td>İsim</td>
      <td>Yaş</td>
    </>
  );
}

Fragment ile Key Kullanımı

Bir listeyi map ile render ederken Fragment'e key prop'u vermek gerekebilir. Bu durumda kısa sözdizimi (<></>) kullanılamaz, tam Fragment sözdizimi gerekir.

function GlossaryList({ items }) {
  return (
    <dl>
      {items.map((item) => (
        <Fragment key={item.id}>
          <dt>{item.term}</dt>
          <dd>{item.description}</dd>
        </Fragment>
      ))}
    </dl>
  );
}

// Tablo satırlarında Fragment
function UserTable({ users }) {
  return (
    <table>
      <tbody>
        {users.map((user) => (
          <Fragment key={user.id}>
            <tr>
              <td>{user.name}</td>
              <td>{user.email}</td>
            </tr>
            {user.notes && (
              <tr>
                <td colSpan={2} className="notes">{user.notes}</td>
              </tr>
            )}
          </Fragment>
        ))}
      </tbody>
    </table>
  );
}

Fragment Kullanım Senaryoları

  • Tablo yapıları: <tr> içinde sadece <td> olmalı, div eklenemez
  • Flex/Grid layout: Gereksiz wrapper, layout'u bozabilir
  • CSS Selectors: Ekstra div, :first-child gibi selector'ları bozar
  • Koşullu render: Birden fazla elemanı koşullu olarak gösterme
  • Semantik HTML: Anlamsız div'lerden kaçınma
// Koşullu render ile Fragment
function UserInfo({ user, isAdmin }) {
  return (
    <div className="user-info">
      <span>{user.name}</span>
      {isAdmin && (
        <>
          <span className="badge">Admin</span>
          <button>Ayarlar</button>
        </>
      )}
    </div>
  );
}

React Portal Nedir?

Portal, bir bileşenin çıktısını DOM ağacının farklı bir noktasına render etmenizi sağlar. Bileşen, React bileşen ağacında aynı yerde kalır (context, event bubbling korunur), ancak DOM çıktısı belirtilen hedefe eklenir.

import { createPortal } from 'react-dom';

function Modal({ isOpen, onClose, children }) {
  if (!isOpen) return null;

  return createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        <button className="modal-close" onClick={onClose}>
          ×
        </button>
        {children}
      </div>
    </div>,
    document.getElementById('modal-root')
  );
}

HTML dosyanızda portal hedefi olarak bir eleman eklemeyi unutmayın:

<!-- index.html -->
<body>
  <div id="root"></div>
  <div id="modal-root"></div>
  <div id="tooltip-root"></div>
</body>

Portal ile Tooltip Bileşeni

import { createPortal } from 'react-dom';
import { useState, useRef, useEffect } from 'react';

function Tooltip({ children, text }) {
  const [isVisible, setIsVisible] = useState(false);
  const [position, setPosition] = useState({ top: 0, left: 0 });
  const triggerRef = useRef(null);

  useEffect(() => {
    if (isVisible && triggerRef.current) {
      const rect = triggerRef.current.getBoundingClientRect();
      setPosition({
        top: rect.top - 8 + window.scrollY,
        left: rect.left + rect.width / 2 + window.scrollX,
      });
    }
  }, [isVisible]);

  return (
    <>
      <span
        ref={triggerRef}
        onMouseEnter={() => setIsVisible(true)}
        onMouseLeave={() => setIsVisible(false)}
      >
        {children}
      </span>

      {isVisible &&
        createPortal(
          <div
            className="tooltip"
            style={{
              position: 'absolute',
              top: position.top,
              left: position.left,
              transform: 'translate(-50%, -100%)',
            }}
          >
            {text}
          </div>,
          document.body
        )}
    </>
  );
}

// Kullanım
<p>
  Bu kelimenin üzerine gelin:{' '}
  <Tooltip text="Bu bir tooltip!">
    <strong>hover me</strong>
  </Tooltip>
</p>

Portal'da Event Bubbling

Portal ile render edilen bileşenler DOM'da farklı bir yerde olsa bile, React event'leri bileşen ağacına göre bubble eder. Bu çok önemli bir özelliktir:

function Parent() {
  const handleClick = () => {
    console.log('Parent click tetiklendi!');
    // Portal içindeki buton tıklandığında da çalışır
  };

  return (
    <div onClick={handleClick}>
      <h2>Parent Bileşen</h2>
      <PortalChild />
    </div>
  );
}

function PortalChild() {
  return createPortal(
    <button>Portal'daki Buton</button>,
    document.getElementById('portal-root')
  );
}
// Butona tıklandığında Parent'ın onClick'i de tetiklenir!
Uyarı: Portal içinde event.stopPropagation() kullanırken dikkatli olun. React event bubbling'i DOM hiyerarşisine değil, React bileşen ağacına göre çalışır. Bu, beklenmedik davranışlara yol açabilir.

Portal ile Notification Sistemi

import { createPortal } from 'react-dom';
import { useState, useCallback } from 'react';

function useNotification() {
  const [notifications, setNotifications] = useState([]);

  const addNotification = useCallback((message, type = 'info') => {
    const id = Date.now();
    setNotifications((prev) => [...prev, { id, message, type }]);

    setTimeout(() => {
      setNotifications((prev) => prev.filter((n) => n.id !== id));
    }, 3000);
  }, []);

  const NotificationContainer = () =>
    createPortal(
      <div className="notification-container">
        {notifications.map((notif) => (
          <div key={notif.id} className={`notification notification-${notif.type}`}>
            {notif.message}
          </div>
        ))}
      </div>,
      document.body
    );

  return { addNotification, NotificationContainer };
}

// Kullanım
function App() {
  const { addNotification, NotificationContainer } = useNotification();

  return (
    <div>
      <button onClick={() => addNotification('Başarılı!', 'success')}>
        Bildirim Göster
      </button>
      <NotificationContainer />
    </div>
  );
}

Server-Side Rendering ve Portal

SSR ortamlarında document nesnesi mevcut değildir. Portal kullanırken bu durumu göz önünde bulundurmalısınız:

function SafePortal({ children, containerId }) {
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) return null;

  const container = document.getElementById(containerId);
  if (!container) return null;

  return createPortal(children, container);
}
İpucu: Fragment ve Portal birlikte kullanılabilir. Örneğin bir Portal içinde birden fazla eleman render ederken Fragment kullanabilirsiniz. Ayrıca erişilebilirlik için modal Portal'larında aria-modal, role="dialog" ve focus trap uygulamayı unutmayın.

Sonuç

React Fragment, gereksiz DOM düğümlerinden kaçınarak temiz ve semantik HTML yapıları oluşturmanızı sağlar. Portal ise bileşenleri DOM hiyerarşisinin dışına render ederek modal, tooltip, dropdown ve bildirim gibi UI elemanlarını doğru şekilde konumlandırmanıza olanak tanır. Her iki özellik de React'in temel yapı taşlarından olup, günlük geliştirmede sıklıkla kullanılır. Fragment'i gereksiz wrapper'lardan kurtulmak, Portal'ı ise overflow ve z-index sorunlarını çözmek için değerlendirin.

Kaynaklar

İlgili Yazılar