React'ta Yeniden Kullanılabilir Modal Bileşeni Oluşturma

Yunus Emre Güzel
6 Ocak 202512 dkReact
React'ta Yeniden Kullanılabilir Modal Bileşeni Oluşturma

React'ta Yeniden Kullanılabilir Modal Bileşeni Oluşturma

Modal bileşenleri, modern web uygulamalarının vazgeçilmez bir parçasıdır. Bu yazıda, React ve TypeScript kullanarak yeniden kullanılabilir, erişilebilir ve animasyonlu bir modal bileşeni oluşturmayı öğreneceğiz.

Modal Bileşeninin Temel Yapısı

İlk olarak, modal bileşenimizin temel yapısını ve prop tiplerini oluşturalım.

import React, { useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import { motion, AnimatePresence } from 'framer-motion';

interface ModalProps {
  isOpen: boolean;
  onClose: () => void;
  title?: string;
  children: React.ReactNode;
  size?: 'sm' | 'md' | 'lg';
  closeOnOutsideClick?: boolean;
  closeOnEsc?: boolean;
}

const Modal: React.FC<ModalProps> = ({
  isOpen,
  onClose,
  title,
  children,
  size = 'md',
  closeOnOutsideClick = true,
  closeOnEsc = true,
}) => {
  const modalRef = useRef<HTMLDivElement>(null);

  // ESC tuşu ile kapatma
  useEffect(() => {
    const handleEscKey = (event: KeyboardEvent) => {
      if (closeOnEsc && event.key === 'Escape') {
        onClose();
      }
    };

    if (isOpen) {
      document.addEventListener('keydown', handleEscKey);
      document.body.style.overflow = 'hidden'; // Scroll'u engelle
    }

    return () => {
      document.removeEventListener('keydown', handleEscKey);
      document.body.style.overflow = 'unset';
    };
  }, [isOpen, onClose, closeOnEsc]);

  // Dışarı tıklama ile kapatma
  const handleOutsideClick = (event: React.MouseEvent) => {
    if (closeOnOutsideClick && modalRef.current && !modalRef.current.contains(event.target as Node)) {
      onClose();
    }
  };

  // Modal içeriği
  const modalContent = (
    <AnimatePresence>
      {isOpen && (
        <div
          className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50"
          onClick={handleOutsideClick}
        >
          <motion.div
            ref={modalRef}
            className={`
              bg-white dark:bg-gray-800 rounded-lg shadow-xl
              ${size === 'sm' ? 'max-w-sm' : size === 'lg' ? 'max-w-2xl' : 'max-w-lg'}
              w-full
            `}
            initial={{ opacity: 0, scale: 0.9, y: 20 }}
            animate={{ opacity: 1, scale: 1, y: 0 }}
            exit={{ opacity: 0, scale: 0.9, y: 20 }}
            transition={{ type: 'spring', duration: 0.3 }}
          >
            {/* Modal Header */}
            {title && (
              <div className="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
                <h2 className="text-xl font-semibold text-gray-900 dark:text-white">
                  {title}
                </h2>
              </div>
            )}

            {/* Modal Body */}
            <div className="px-6 py-4">{children}</div>

            {/* Modal Footer - İsteğe bağlı */}
            <div className="px-6 py-4 border-t border-gray-200 dark:border-gray-700">
              <div className="flex justify-end space-x-3">
                <button
                  onClick={onClose}
                  className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600"
                >
                  Kapat
                </button>
              </div>
            </div>
          </motion.div>
        </div>
      )}
    </AnimatePresence>
  );

  // Portal ile body'e render etme
  return createPortal(modalContent, document.body);
};

export default Modal;

Modal Hook'u ile State Yönetimi

Modal'ın state yönetimini kolaylaştırmak için özel bir hook oluşturalım.

import { useState, useCallback } from 'react';

interface UseModalReturn {
  isOpen: boolean;
  open: () => void;
  close: () => void;
  toggle: () => void;
}

export function useModal(initialState = false): UseModalReturn {
  const [isOpen, setIsOpen] = useState(initialState);

  const open = useCallback(() => setIsOpen(true), []);
  const close = useCallback(() => setIsOpen(false), []);
  const toggle = useCallback(() => setIsOpen(prev => !prev), []);

  return { isOpen, open, close, toggle };
}

Erişilebilirlik (A11y) Özellikleri

Modal'ımızı ARIA standartlarına uygun hale getirelim.

interface ModalProps {
  // ... diğer prop'lar ...
  ariaLabel?: string;
  ariaDescribedby?: string;
}

const Modal: React.FC<ModalProps> = ({
  // ... diğer prop'lar ...
  ariaLabel,
  ariaDescribedby,
}) => {
  // ... önceki kodlar ...

  const modalContent = (
    <div
      role="dialog"
      aria-modal="true"
      aria-label={ariaLabel}
      aria-describedby={ariaDescribedby}
      className="modal-container"
    >
      {/* ... modal içeriği ... */}
    </div>
  );

  // ... kalan kodlar ...
};

Özelleştirilebilir Animasyonlar

Framer Motion ile farklı animasyon varyantları ekleyelim.

const animationVariants = {
  fade: {
    initial: { opacity: 0 },
    animate: { opacity: 1 },
    exit: { opacity: 0 },
  },
  scale: {
    initial: { opacity: 0, scale: 0.9 },
    animate: { opacity: 1, scale: 1 },
    exit: { opacity: 0, scale: 0.9 },
  },
  slideUp: {
    initial: { opacity: 0, y: 50 },
    animate: { opacity: 1, y: 0 },
    exit: { opacity: 0, y: 50 },
  },
};

interface ModalProps {
  // ... diğer prop'lar ...
  animation?: keyof typeof animationVariants;
}

const Modal: React.FC<ModalProps> = ({
  // ... diğer prop'lar ...
  animation = 'scale',
}) => {
  const selectedAnimation = animationVariants[animation];

  return (
    <motion.div
      initial={selectedAnimation.initial}
      animate={selectedAnimation.animate}
      exit={selectedAnimation.exit}
      transition={{ type: 'spring', duration: 0.3 }}
    >
      {/* ... modal içeriği ... */}
    </motion.div>
  );
};

Kullanım Örnekleri

Modal bileşenimizi farklı senaryolarda nasıl kullanabileceğimizi görelim.

1. Basit Kullanım

function App() {
  const { isOpen, open, close } = useModal();

  return (
    <div>
      <button onClick={open}>Modal'ı Aç</button>
      
      <Modal isOpen={isOpen} onClose={close} title="Örnek Modal">
        <p>Modal içeriği burada yer alır.</p>
      </Modal>
    </div>
  );
}

2. Form Modal'ı

function UserFormModal() {
  const { isOpen, close } = useModal();
  const [formData, setFormData] = useState({ name: '', email: '' });

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      await saveUser(formData);
      close();
    } catch (error) {
      console.error('Form gönderimi başarısız:', error);
    }
  };

  return (
    <Modal
      isOpen={isOpen}
      onClose={close}
      title="Kullanıcı Ekle"
      size="lg"
      closeOnOutsideClick={false}
    >
      <form onSubmit={handleSubmit} className="space-y-4">
        <div>
          <label className="block text-sm font-medium text-gray-700">
            Ad Soyad
          </label>
          <input
            type="text"
            value={formData.name}
            onChange={e => setFormData(prev => ({ ...prev, name: e.target.value }))}
            className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
          />
        </div>
        
        <div>
          <label className="block text-sm font-medium text-gray-700">
            E-posta
          </label>
          <input
            type="email"
            value={formData.email}
            onChange={e => setFormData(prev => ({ ...prev, email: e.target.value }))}
            className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
          />
        </div>

        <div className="flex justify-end space-x-3">
          <button
            type="button"
            onClick={close}
            className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md"
          >
            İptal
          </button>
          <button
            type="submit"
            className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md"
          >
            Kaydet
          </button>
        </div>
      </form>
    </Modal>
  );
}

3. Onay Modal'ı

function ConfirmationModal({ isOpen, onClose, onConfirm, message }: {
  isOpen: boolean;
  onClose: () => void;
  onConfirm: () => void;
  message: string;
}) {
  return (
    <Modal
      isOpen={isOpen}
      onClose={onClose}
      title="Onay"
      size="sm"
      animation="slideUp"
    >
      <div className="text-center">
        <p className="text-gray-700 dark:text-gray-300">{message}</p>
        
        <div className="mt-6 flex justify-center space-x-3">
          <button
            onClick={onClose}
            className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md"
          >
            İptal
          </button>
          <button
            onClick={() => {
              onConfirm();
              onClose();
            }}
            className="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-md"
          >
            Onayla
          </button>
        </div>
      </div>
    </Modal>
  );
}

Best Practices ve İpuçları

  1. Performans Optimizasyonu

    • Modal içeriğini lazy loading ile yükleyin
    • Gereksiz render'ları önlemek için React.memo kullanın
    • Animasyonlar için CSS transforms kullanın
  2. Erişilebilirlik

    • Focus trap kullanın
    • Uygun ARIA attribute'larını ekleyin
    • Klavye navigasyonunu destekleyin
  3. Responsive Tasarım

    • Mobile-first yaklaşımı benimseyin
    • Farklı ekran boyutları için uygun padding ve margin değerleri kullanın
    • Touch cihazlar için uygun interaction'ları ekleyin
  4. State Yönetimi

    • Complex state için useReducer kullanın
    • Form state'i için React Hook Form gibi kütüphaneler tercih edin
    • Global state gerekiyorsa Context API veya state management kütüphanesi kullanın

Sonuç

Modern bir modal bileşeni oluştururken dikkat edilmesi gereken birçok nokta vardır:

  • Erişilebilirlik standartlarına uygunluk
  • Performans optimizasyonu
  • Responsive tasarım
  • Kullanıcı deneyimi
  • Yeniden kullanılabilirlik
  • Type safety

Bu yazıda oluşturduğumuz modal bileşeni, tüm bu gereksinimleri karşılayan, production-ready bir çözüm sunmaktadır.

İlgili Etiketler: #React #Components #TypeScript #Accessibility #Animation #UIUX #WebDevelopment

Kaynaklar