Ders İçeriği

Kontrollü Bileşenler (Controlled Components)

React'te form yönetimi yaparken en önemli kavramlardan biri kontrollü bileşenlerdir. Kontrollü bileşenler, form elemanlarının değerlerinin React state'i tarafından kontrol edildiği bileşenlerdir. Bu yaklaşımda, form elemanının değeri her zaman React state'inden gelir ve değişiklikler state güncelleme fonksiyonları aracılığıyla yapılır.

Kontrollü bileşenlerin temel prensibi, "single source of truth" (tek doğruluk kaynağı) kavramına dayanır. Form elemanının değeri sadece React state'inde tutulur ve DOM'daki değer bu state'ten türetilir. Bu sayede, form verilerinin durumu her zaman öngörülebilir ve kontrol edilebilir olur.

Basit bir kontrollü input örneği şu şekildedir:

import React, { useState } from 'react';


function ControlledInput() {

  const [value, setValue] = useState('');

  

  const handleChange = (event) => {

    setValue(event.target.value);

  };

  

  return (

    <div>

      <input 

        type="text"

        value={value}

        onChange={handleChange}

        placeholder="Adınızı girin"

      />

      <p>Girilen değer: {value}</p>

    </div>

  );

}

Bu örnekte, input elemanının değeri value state'inden gelir ve kullanıcı her karakter yazdığında onChange event handler'ı çalışarak state'i günceller. Bu sayede, input'un değeri her zaman React state'i ile senkronize kalır.

Kontrollü bileşenlerin avantajları oldukça fazladır. İlk olarak, form verilerine her zaman erişiminiz vardır ve bu verileri istediğiniz zaman kullanabilirsiniz. İkinci olarak, form validasyonunu gerçek zamanlı olarak yapabilirsiniz. Üçüncü olarak, form verilerini başka bileşenlerle paylaşmak çok kolaydır. Son olarak, form verilerini test etmek ve debug yapmak daha kolaydır.

Daha karmaşık bir kontrollü form örneği:

function UserRegistrationForm() {

  const [formData, setFormData] = useState({

    firstName: '',

    lastName: '',

    email: '',

    password: '',

    confirmPassword: '',

    agreeToTerms: false

  });

  

  const handleInputChange = (event) => {

    const { name, value, type, checked } = event.target;

    setFormData(prevData => ({

      ...prevData,

      [name]: type === 'checkbox' ? checked : value

    }));

  };

  

  const handleSubmit = (event) => {

    event.preventDefault();

    console.log('Form verileri:', formData);

  };

  

  return (

    <form onSubmit={handleSubmit}>

      <div>

        <label>

          Ad:

          <input

            type="text"

            name="firstName"

            value={formData.firstName}

            onChange={handleInputChange}

            required

          />

        </label>

      </div>

      

      <div>

        <label>

          Soyad:

          <input

            type="text"

            name="lastName"

            value={formData.lastName}

            onChange={handleInputChange}

            required

          />

        </label>

      </div>

      

      <div>

        <label>

          E-posta:

          <input

            type="email"

            name="email"

            value={formData.email}

            onChange={handleInputChange}

            required

          />

        </label>

      </div>

      

      <div>

        <label>

          Şifre:

          <input

            type="password"

            name="password"

            value={formData.password}

            onChange={handleInputChange}

            required

          />

        </label>

      </div>

      

      <div>

        <label>

          Şifre Tekrar:

          <input

            type="password"

            name="confirmPassword"

            value={formData.confirmPassword}

            onChange={handleInputChange}

            required

          />

        </label>

      </div>

      

      <div>

        <label>

          <input

            type="checkbox"

            name="agreeToTerms"

            checked={formData.agreeToTerms}

            onChange={handleInputChange}

            required

          />

          Kullanım şartlarını kabul ediyorum

        </label>

      </div>

      

      <button type="submit">Kayıt Ol</button>

    </form>

  );

}

Bu örnekte, tüm form verileri tek bir state objesi içinde tutulur ve handleInputChange fonksiyonu tüm input elemanları için kullanılır. Bu yaklaşım, kod tekrarını azaltır ve form yönetimini daha verimli hale getirir.

Kontrolsüz Bileşenler (Uncontrolled Components)

Kontrolsüz bileşenler, form elemanlarının değerlerinin DOM tarafından yönetildiği bileşenlerdir. Bu yaklaşımda, React state'i kullanmak yerine, DOM'un kendi internal state'ini kullanırız ve form verilerine ihtiyaç duyduğumuzda DOM'dan okuruz.

Kontrolsüz bileşenlerde, form elemanlarına erişmek için useRef hook'u kullanılır:

import React, { useRef } from 'react';


function UncontrolledForm() {

  const nameRef = useRef();

  const emailRef = useRef();

  

  const handleSubmit = (event) => {

    event.preventDefault();

    const formData = {

      name: nameRef.current.value,

      email: emailRef.current.value

    };

    console.log('Form verileri:', formData);

  };

  

  return (

    <form onSubmit={handleSubmit}>

      <div>

        <label>

          Ad:

          <input

            type="text"

            ref={nameRef}

            defaultValue="Varsayılan Ad"

          />

        </label>

      </div>

      

      <div>

        <label>

          E-posta:

          <input

            type="email"

            ref={emailRef}

          />

        </label>

      </div>

      

      <button type="submit">Gönder</button>

    </form>

  );

}

Kontrolsüz bileşenlerde value prop'u yerine defaultValue prop'u kullanılır. Bu, elemanın başlangıç değerini belirler ancak sonrasında React bu değeri kontrol etmez.

Kontrolsüz bileşenlerin avantajları şunlardır: daha az kod yazımı gerektirir, performans açısından daha verimli olabilir (özellikle büyük formlarda), üçüncü parti kütüphanelerle entegrasyon daha kolaydır. Ancak dezavantajları da vardır: gerçek zamanlı validasyon yapmak zordur, form verilerine sürekli erişim yoktur, test etmek daha zordur.

Form Verilerini Yakalama

Form verilerini yakalamak için çeşitli yöntemler kullanılabilir. En yaygın yöntemler kontrollü bileşenler, kontrolsüz bileşenler ve FormData API'sidir.

FormData API Kullanımı

Modern tarayıcılarda desteklenen FormData API'si, form verilerini kolayca yakalamak için kullanılabilir:

function FormDataExample() {

  const handleSubmit = (event) => {

    event.preventDefault();

    const formData = new FormData(event.target);

    

    // Form verilerini obje olarak al

    const data = Object.fromEntries(formData.entries());

    console.log('Form verileri:', data);

    

    // Dosya yükleme için

    const file = formData.get('avatar');

    if (file) {

      console.log('Yüklenen dosya:', file.name);

    }

  };

  

  return (

    <form onSubmit={handleSubmit}>

      <input name="firstName" placeholder="Ad" required />

      <input name="lastName" placeholder="Soyad" required />

      <input name="email" type="email" placeholder="E-posta" required />

      <input name="avatar" type="file" accept="image/*" />

      <select name="country">

        <option value="tr">Türkiye</option>

        <option value="us">Amerika</option>

        <option value="de">Almanya</option>

      </select>

      <textarea name="message" placeholder="Mesajınız"></textarea>

      <button type="submit">Gönder</button>

    </form>

  );

}

Özel Form Hook'u

Form yönetimini daha verimli hale getirmek için özel bir hook oluşturabilirsiniz:

function useForm(initialValues, validate) {

  const [values, setValues] = useState(initialValues);

  const [errors, setErrors] = useState({});

  const [touched, setTouched] = useState({});

  const [isSubmitting, setIsSubmitting] = useState(false);

  

  const handleChange = (event) => {

    const { name, value, type, checked } = event.target;

    const newValue = type === 'checkbox' ? checked : value;

    

    setValues(prev => ({

      ...prev,

      [name]: newValue

    }));

    

    // Gerçek zamanlı validasyon

    if (validate && touched[name]) {

      const fieldErrors = validate({ ...values, [name]: newValue });

      setErrors(prev => ({

        ...prev,

        [name]: fieldErrors[name]

      }));

    }

  };

  

  const handleBlur = (event) => {

    const { name } = event.target;

    setTouched(prev => ({

      ...prev,

      [name]: true

    }));

    

    // Blur olayında validasyon

    if (validate) {

      const fieldErrors = validate(values);

      setErrors(prev => ({

        ...prev,

        [name]: fieldErrors[name]

      }));

    }

  };

  

  const handleSubmit = async (onSubmit) => {

    setIsSubmitting(true);

    

    // Tüm alanları touched olarak işaretle

    const allTouched = Object.keys(values).reduce((acc, key) => {

      acc[key] = true;

      return acc;

    }, {});

    setTouched(allTouched);

    

    // Validasyon kontrolü

    if (validate) {

      const formErrors = validate(values);

      setErrors(formErrors);

      

      const hasErrors = Object.keys(formErrors).some(key => formErrors[key]);

      if (hasErrors) {

        setIsSubmitting(false);

        return;

      }

    }

    

    try {

      await onSubmit(values);

    } catch (error) {

      console.error('Form gönderimi başarısız:', error);

    } finally {

      setIsSubmitting(false);

    }

  };

  

  const reset = () => {

    setValues(initialValues);

    setErrors({});

    setTouched({});

    setIsSubmitting(false);

  };

  

  return {

    values,

    errors,

    touched,

    isSubmitting,

    handleChange,

    handleBlur,

    handleSubmit,

    reset

  };

}


// Kullanımı

function ContactForm() {

  const initialValues = {

    name: '',

    email: '',

    message: ''

  };

  

  const validate = (values) => {

    const errors = {};

    

    if (!values.name) {

      errors.name = 'İsim gerekli';

    } else if (values.name.length < 2) {

      errors.name = 'İsim en az 2 karakter olmalı';

    }

    

    if (!values.email) {

      errors.email = 'E-posta gerekli';

    } else if (!/\S+@\S+\.\S+/.test(values.email)) {

      errors.email = 'Geçersiz e-posta formatı';

    }

    

    if (!values.message) {

      errors.message = 'Mesaj gerekli';

    } else if (values.message.length < 10) {

      errors.message = 'Mesaj en az 10 karakter olmalı';

    }

    

    return errors;

  };

  

  const {

    values,

    errors,

    touched,

    isSubmitting,

    handleChange,

    handleBlur,

    handleSubmit,

    reset

  } = useForm(initialValues, validate);

  

  const onSubmit = async (formData) => {

    // API çağrısı simülasyonu

    await new Promise(resolve => setTimeout(resolve, 1000));

    console.log('Form gönderildi:', formData);

    alert('Mesajınız başarıyla gönderildi!');

    reset();

  };

  

  return (

    <form onSubmit={(e) => {

      e.preventDefault();

      handleSubmit(onSubmit);

    }}>

      <div>

        <label>

          İsim:

          <input

            type="text"

            name="name"

            value={values.name}

            onChange={handleChange}

            onBlur={handleBlur}

          />

        </label>

        {touched.name && errors.name && (

          <span style={{ color: 'red' }}>{errors.name}</span>

        )}

      </div>

      

      <div>

        <label>

          E-posta:

          <input

            type="email"

            name="email"

            value={values.email}

            onChange={handleChange}

            onBlur={handleBlur}

          />

        </label>

        {touched.email && errors.email && (

          <span style={{ color: 'red' }}>{errors.email}</span>

        )}

      </div>

      

      <div>

        <label>

          Mesaj:

          <textarea

            name="message"

            value={values.message}

            onChange={handleChange}

            onBlur={handleBlur}

            rows="4"

          />

        </label>

        {touched.message && errors.message && (

          <span style={{ color: 'red' }}>{errors.message}</span>

        )}

      </div>

      

      <button type="submit" disabled={isSubmitting}>

        {isSubmitting ? 'Gönderiliyor...' : 'Gönder'}

      </button>

      

      <button type="button" onClick={reset}>

        Temizle

      </button>

    </form>

  );

}

Form Doğrulama (Validation)

Form doğrulama, kullanıcı deneyimi ve veri kalitesi açısından kritik bir konudur. React'te form doğrulama çeşitli seviyelerde yapılabilir: client-side validation, server-side validation ve real-time validation.

Client-Side Validation

Client-side validation, kullanıcı deneyimini iyileştirmek için yapılır ve anında geri bildirim sağlar:

function AdvancedValidationForm() {

  const [formData, setFormData] = useState({

    username: '',

    email: '',

    password: '',

    confirmPassword: '',

    age: '',

    website: ''

  });

  

  const [errors, setErrors] = useState({});

  const [touched, setTouched] = useState({});

  

  const validationRules = {

    username: {

      required: true,

      minLength: 3,

      maxLength: 20,

      pattern: /^[a-zA-Z0-9_]+$/,

      message: 'Kullanıcı adı 3-20 karakter arası olmalı ve sadece harf, rakam, _ içermeli'

    },

    email: {

      required: true,

      pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,

      message: 'Geçerli bir e-posta adresi girin'

    },

    password: {

      required: true,

      minLength: 8,

      pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/,

      message: 'Şifre en az 8 karakter, büyük/küçük harf, rakam ve özel karakter içermeli'

    },

    confirmPassword: {

      required: true,

      custom: (value, formData) => value === formData.password,

      message: 'Şifreler eşleşmiyor'

    },

    age: {

      required: true,

      min: 18,

      max: 120,

      message: 'Yaş 18-120 arasında olmalı'

    },

    website: {

      pattern: /^https?:\/\/.+/,

      message: 'Geçerli bir URL girin (http:// veya https:// ile başlamalı)'

    }

  };

  

  const validateField = (name, value, allData = formData) => {

    const rules = validationRules[name];

    if (!rules) return '';

    

    // Required kontrolü

    if (rules.required && (!value || value.toString().trim() === '')) {

      return `${name} alanı gerekli`;

    }

    

    // Eğer alan boş ve required değilse, diğer kontrolleri yapma

    if (!value || value.toString().trim() === '') {

      return '';

    }

    

    // MinLength kontrolü

    if (rules.minLength && value.length < rules.minLength) {

      return `En az ${rules.minLength} karakter olmalı`;

    }

    

    // MaxLength kontrolü

    if (rules.maxLength && value.length > rules.maxLength) {

      return `En fazla ${rules.maxLength} karakter olmalı`;

    }

    

    // Pattern kontrolü

    if (rules.pattern && !rules.pattern.test(value)) {

      return rules.message || 'Geçersiz format';

    }

    

    // Min/Max kontrolü (sayısal değerler için)

    if (rules.min !== undefined && Number(value) < rules.min) {

      return `En az ${rules.min} olmalı`;

    }

    

    if (rules.max !== undefined && Number(value) > rules.max) {

      return `En fazla ${rules.max} olmalı`;

    }

    

    // Özel validasyon

    if (rules.custom && !rules.custom(value, allData)) {

      return rules.message || 'Geçersiz değer';

    }

    

    return '';

  };

  

  const handleChange = (event) => {

    const { name, value } = event.target;

    

    setFormData(prev => ({

      ...prev,

      [name]: value

    }));

    

    // Gerçek zamanlı validasyon (sadece touched alanlar için)

    if (touched[name]) {

      const error = validateField(name, value, { ...formData, [name]: value });

      setErrors(prev => ({

        ...prev,

        [name]: error

      }));

    }

  };

  

  const handleBlur = (event) => {

    const { name, value } = event.target;

    

    setTouched(prev => ({

      ...prev,

      [name]: true

    }));

    

    const error = validateField(name, value);

    setErrors(prev => ({

      ...prev,

      [name]: error

    }));

  };

  

  const handleSubmit = (event) => {

    event.preventDefault();

    

    // Tüm alanları validate et

    const newErrors = {};

    Object.keys(validationRules).forEach(field => {

      const error = validateField(field, formData[field]);

      if (error) {

        newErrors[field] = error;

      }

    });

    

    setErrors(newErrors);

    setTouched(Object.keys(validationRules).reduce((acc, key) => {

      acc[key] = true;

      return acc;

    }, {}));

    

    // Hata yoksa form gönder

    if (Object.keys(newErrors).length === 0) {

      console.log('Form başarıyla gönderildi:', formData);

      alert('Kayıt başarılı!');

    }

  };

  

  return (

    <form onSubmit={handleSubmit}>

      <div>

        <label>

          Kullanıcı Adı:

          <input

            type="text"

            name="username"

            value={formData.username}

            onChange={handleChange}

            onBlur={handleBlur}

          />

        </label>

        {touched.username && errors.username && (

          <div style={{ color: 'red', fontSize: '0.8em' }}>

            {errors.username}

          </div>

        )}

      </div>

      

      <div>

        <label>

          E-posta:

          <input

            type="email"

            name="email"

            value={formData.email}

            onChange={handleChange}

            onBlur={handleBlur}

          />

        </label>

        {touched.email && errors.email && (

          <div style={{ color: 'red', fontSize: '0.8em' }}>

            {errors.email}

          </div>

        )}

      </div>

      

      <div>

        <label>

          Şifre:

          <input

            type="password"

            name="password"

            value={formData.password}

            onChange={handleChange}

            onBlur={handleBlur}

          />

        </label>

        {touched.password && errors.password && (

          <div style={{ color: 'red', fontSize: '0.8em' }}>

            {errors.password}

          </div>

        )}

      </div>

      

      <div>

        <label>

          Şifre Tekrar:

          <input

            type="password"

            name="confirmPassword"

            value={formData.confirmPassword}

            onChange={handleChange}

            onBlur={handleBlur}

          />

        </label>

        {touched.confirmPassword && errors.confirmPassword && (

          <div style={{ color: 'red', fontSize: '0.8em' }}>

            {errors.confirmPassword}

          </div>

        )}

      </div>

      

      <div>

        <label>

          Yaş:

          <input

            type="number"

            name="age"

            value={formData.age}

            onChange={handleChange}

            onBlur={handleBlur}

          />

        </label>

        {touched.age && errors.age && (

          <div style={{ color: 'red', fontSize: '0.8em' }}>

            {errors.age}

          </div>

        )}

      </div>

      

      <div>

        <label>

          Web Sitesi (Opsiyonel):

          <input

            type="url"

            name="website"

            value={formData.website}

            onChange={handleChange}

            onBlur={handleBlur}

            placeholder="https://example.com"

          />

        </label>

        {touched.website && errors.website && (

          <div style={{ color: 'red', fontSize: '0.8em' }}>

            {errors.website}

          </div>

        )}

      </div>

      

      <button type="submit">Kayıt Ol</button>

    </form>

  );

}

Async Validation

Bazı durumlarda, validasyon için server'a istek atmak gerekebilir (örneğin, kullanıcı adının benzersiz olup olmadığını kontrol etmek):

function AsyncValidationExample() {

  const [username, setUsername] = useState('');

  const [usernameStatus, setUsernameStatus] = useState('idle'); // idle, checking, available, taken

  const [debounceTimer, setDebounceTimer] = useState(null);

  

  const checkUsernameAvailability = async (username) => {

    // API çağrısı simülasyonu

    await new Promise(resolve => setTimeout(resolve, 1000));

    

    // Simülasyon: 'admin' ve 'test' kullanıcı adları alınmış

    const takenUsernames = ['admin', 'test', 'user'];

    return !takenUsernames.includes(username.toLowerCase());

  };

  

  const handleUsernameChange = (event) => {

    const value = event.target.value;

    setUsername(value);

    

    // Önceki timer'ı temizle

    if (debounceTimer) {

      clearTimeout(debounceTimer);

    }

    

    if (value.length >= 3) {

      setUsernameStatus('checking');

      

      // Debounce: 500ms bekle, sonra kontrol et

      const timer = setTimeout(async () => {

        try {

          const isAvailable = await checkUsernameAvailability(value);

          setUsernameStatus(isAvailable ? 'available' : 'taken');

        } catch (error) {

          setUsernameStatus('error');

        }

      }, 500);

      

      setDebounceTimer(timer);

    } else {

      setUsernameStatus('idle');

    }

  };

  

  const getUsernameStatusMessage = () => {

    switch (usernameStatus) {

      case 'checking':

        return { message: 'Kontrol ediliyor...', color: 'orange' };

      case 'available':

        return { message: 'Kullanıcı adı uygun!', color: 'green' };

      case 'taken':

        return { message: 'Bu kullanıcı adı alınmış', color: 'red' };

      case 'error':

        return { message: 'Kontrol edilemedi', color: 'red' };

      default:

        return null;

    }

  };

  

  const statusMessage = getUsernameStatusMessage();

  

  return (

    <div>

      <label>

        Kullanıcı Adı:

        <input

          type="text"

          value={username}

          onChange={handleUsernameChange}

          placeholder="En az 3 karakter"

        />

      </label>

      {statusMessage && (

        <div style={{ color: statusMessage.color, fontSize: '0.8em' }}>

          {statusMessage.message}

        </div>

      )}

    </div>

  );

}

React'te form yönetimi, kullanıcı deneyimi ve veri kalitesi açısından kritik bir konudur. Kontrollü bileşenler genellikle tercih edilir çünkü daha fazla kontrol ve esneklik sağlar. Form validasyonu, hem client-side hem de server-side olarak yapılmalıdır. Modern React uygulamalarında, form yönetimini kolaylaştırmak için Formik, React Hook Form gibi kütüphaneler de kullanılabilir, ancak temel kavramları anlamak her zaman önemlidir.