Merhabalar, oluşturacağım bu içerikle sizlere React projelerinde kullanılan custom hook hakkında genel bir bilgi verdikten sonra kullanılma mantığı ve sebeplerinin daha iyi anlaşılması için bir form validasyonu örneği göstereceğim.
Custom Hook Nedir?
İlk olarak Hook kavramına bakacak olursak, React’ın 16.8 versiyonu ile birlikte karşımıza çıkan, sınıf bileşeni (class component) oluşturmadan state ve diğer React özelliklerini fonksiyon bileşenlerinde (function component) kullanmamıza olanak veren yapılardır. Örneğin; state yönetimini fonksiyon bileşeninde basitçe yapmamıza olanak veren, React üzerinde tanımlı useState hook’u vardır. useMemo, useEffect, useCallback vb. React üzerinde kullanılan diğer hook türleridir.
Bir custom hook ise basit olarak bileşenlerde aynı mantıktaki farklı ihtiyaçlardan dolayı tekrarlanan kod yapılarını ortadan kaldırıp, bu ihtiyaçlar için generic (genel) bir yapı ortaya koyan, biz geliştiriciler tarafından oluşturulan ve React üzerinde tanımlı useState vb. hook’ları da içerisinde kullanmaya imkan veren özel hook’lardır. React dokümantasyonuna baktığımızda zorunlu olmamakla birlikte “use” ön eki ile tanımlanması gerektiği belirtilmiştir. Bir custom hook, onu kullanacak olan bileşenlerdeki ihtiyaçlarımıza göre bizim belirlediğimiz değerleri parametre olarak alır ve bizim belirlediğimiz değerleri asıl bileşenimize geri döndürür. Kullanım mantığını ve sebeplerini daha iyi anlamak için form validasyonu örneğine bakalım.
Form Validasyonu Örneği
Kullanıcı adı, e-mail ve şifreden oluşan bir form yapısını düşünelim. Formu kullanıcı deneyimi açısından nispeten iyi bir duruma getirmek için basitçe aşağıdaki özelliklerde oluştururuz.
- Kullanıcı adı kısmı seçildikten sonra boş bırakıldıysa uyarı verilmelidir.
- E-mail kısmı seçildikten sonra ‘@’ sembolü olmadan bırakıldıysa uyarı verilmelidir.
- Şifre seçildikten sonra 7 karakterden daha az uzunlukta bırakıldıysa uyarı verilmelidir.
- İlk 3 maddedeki şartlar sağlanmadıysa Gönder butonu aktif olmamalıdır.
Aşağıda formun geçersiz olduğu durumla ilgili bir görüntü paylaşılmıştır. Sayfanın render edilmesi ile tüm girişlerin boş olmasına rağmen uyarı mesajlarının verilmemesi gerektiğine ve yukarıdaki maddelerdeki “seçildikten sonra” ifadelerine dikkat etmek gerekir. Bu özelliklerin sağlanması için input yapılarında onChange ve onBlur özelliklerinin ikisi birden kullanılıp, kullanıcın her tuş darbesinde geçerli ya da geçersiz durumlar arasında geçiş sağlanmıştır.

Burada ilk olarak custom hook tanımını yaparken “… aynı mantıktaki farklı ihtiyaçlardan dolayı tekrarlanan kod yapılarını ortadan kaldırıp” ifadesini örneğimiz için açalım. Formun 3 giriş elemanı farklı validasyon kurallarını gerektirse de bahsedilen kullanıcı deneyiminin sağlanması için aynı mantıktaki kodların ilgili Input bileşeninde tekrarlanması gerekmektedir. Aşağıda istenilen kullanıcı deneyimine sahip ve oluşturacağımız custom hook’u kullanmadan yapılan form örneği kodları verilmiştir.
import { useState } from "react"; const SimpleInput = (props) => { const [enteredUsername, setEnteredUsername] = useState(""); const [enteredUsernameTouched, setEnteredUsernameTouched] = useState(false); const [enteredEmail, setEnteredEmail] = useState(""); const [enteredEmailTouched, setEnteredEmailTouched] = useState(false); const [enteredPassword, setEnteredPassword] = useState(""); const [enteredPasswordTouched, setEnteredPasswordTouched] = useState(false); const enteredUsernameIsValid = enteredUsername.trim() !== ""; const nameInputIsInvalid = !enteredUsernameIsValid && enteredUsernameTouched; const enteredEmailIsValid = enteredEmail.includes("@"); const emailInputIsInvalid = !enteredEmailIsValid && enteredEmailTouched; const enteredPasswordIsValid = enteredPassword.length > 6; const passwordInputIsInvalid = !enteredPasswordIsValid && enteredPasswordTouched; let formIsValid = false; if (enteredUsernameIsValid && enteredEmailIsValid && enteredPasswordIsValid) formIsValid = true; const usernameInputChangeHandler = (event) => { setEnteredUsername(event.target.value); }; const usernameInputBlurHandler = (event) => { setEnteredUsernameTouched(true); }; const emailInputChangeHandler = (event) => { setEnteredEmail(event.target.value); }; const emailInputBlurHandler = (event) => { setEnteredEmailTouched(true); }; const passwordInputChangeHandler = (event) => { setEnteredPassword(event.target.value); }; const passwordInputBlurHandler = (event) => { setEnteredPasswordTouched(true); }; const formSubmissionHandler = (event) => { event.preventDefault(); console.log("Form gönderildi."); console.log(enteredUsername, enteredEmail, enteredPassword); setEnteredUsername(""); setEnteredUsernameTouched(false); setEnteredEmail(""); setEnteredEmailTouched(false); setEnteredPassword(""); setEnteredPasswordTouched(false); }; const nameInputClasses = nameInputIsInvalid ? "form-control invalid" : "form-control"; const emailInputClasses = emailInputIsInvalid ? "form-control invalid" : "form-control"; const passwordInputClasses = passwordInputIsInvalid ? "form-control invalid" : "form-control"; return ( <form onSubmit={formSubmissionHandler}> <div className={nameInputClasses}> <label htmlFor="name">Ad</label> <input type="text" id="name" onChange={usernameInputChangeHandler} onBlur={usernameInputBlurHandler} value={enteredUsername} /> {nameInputIsInvalid && ( <p className="error-text">Kullanıcı adı boş olamaz.</p> )} </div> <div className={emailInputClasses}> <label htmlFor="email">E-Mail</label> <input type="email" id="email" onChange={emailInputChangeHandler} onBlur={emailInputBlurHandler} value={enteredEmail} /> {emailInputIsInvalid && ( <p className="error-text">Geçerli bir email adresi giriniz.</p> )} </div> <div className={passwordInputClasses}> <label htmlFor="password">Şifre</label> <input type="password" id="password" onChange={passwordInputChangeHandler} onBlur={passwordInputBlurHandler} value={enteredPassword} /> {passwordInputIsInvalid && ( <p className="error-text">Şifre en az 7 karakter uzunluğunda olmalıdır.</p> )} </div> <div className="form-actions"> <button disabled={!formIsValid}>Gönder</button> </div> </form> ); }; export default SimpleInput;
Çalışmaya baktığımızda kullanıcı adı, e-mail ve şifre için benzer useState tanımlamalarının, change ve blur handler fonksiyonlarının tanımlamalarının ayrı ayrı tekrarlandığını görüyoruz. Bileşen kullanıcı deneyiminin gerekliliklerini yerine getirse de basit işlevlerine rağmen kod düzeni olarak yeterince yalın ve yönetilebilir olmayıp, API mantığına benzer bir şekilde dışarıdan generic bir yapıya ihtiyaç duymaktadır.
Custom Hook İle Form Validasyonu Örneği
Benzer kod yapılarına sahip 3 input değeri için generic bir yapının oluşturulup, sadece her input için farklı olan validasyon fonksiyonlarını parametre olarak alan ve input’un geriye girilen değerini, geçerlilik durumunu, hataya sahiplik durumunu, change, blur ve form gönderiminden sonra resetleme fonksiyonlarını geriye döndüren useInput custom hook’unu aşağıdaki gibi oluştururuz.
import { useState } from "react"; const useInput = (validateValue) => { const [enteredValue, setEnteredValue] = useState(""); const [isTouched, setIsTouched] = useState(false); const valueIsValid = validateValue(enteredValue); const hasError = !valueIsValid && isTouched; const valueChangeHandler = (event) => { setEnteredValue(event.target.value); }; const inputBlurHandler = (event) => { setIsTouched(true); }; const reset = () => { setEnteredValue(""); setIsTouched(false); }; return { value: enteredValue, isValid: valueIsValid, hasError, valueChangeHandler, inputBlurHandler, reset, }; }; export default useInput;
Daha sonra Input bileşenini aşağıdaki gibi oluşturarak input değerlerine göre istediğimiz tüm değerleri custom hook üzerinden kullanarak bileşenimizi daha basit ve yönetebilir hale getirip, tekrar eden useState ve fonksiyon tanımlamalarını ortadan kaldırırız.
import useInput from "../hooks/use-input"; const SimpleInput = (props) => { const { value: enteredUsername, isValid: enteredUsernameIsValid, hasError: usernameInputHasError, valueChangeHandler: usernameChangedHandler, inputBlurHandler: usernameBlurHandler, reset: resetUsernameInput, } = useInput((value) => value.trim() !== ""); const { value: enteredEmail, isValid: enteredEmailIsValid, hasError: emailInputHasError, valueChangeHandler: emailChangedHandler, inputBlurHandler: emailBlurHandler, reset: resetEmailInput, } = useInput((value) => value.includes("@")); const { value: enteredPassword, isValid: enteredPasswordIsValid, hasError: passwordInputHasError, valueChangeHandler: passwordChangedHandler, inputBlurHandler: passwordBlurHandler, reset: resetPasswordInput, } = useInput((value) => value.length > 6); let formIsValid = false; if (enteredUsernameIsValid && enteredEmailIsValid && enteredPasswordIsValid) formIsValid = true; const formSubmissionHandler = (event) => { event.preventDefault(); console.log("Form gönderildi."); console.log(enteredUsername, enteredEmail, enteredPassword); resetUsernameInput(); resetEmailInput(); resetPasswordInput(); }; const usernameInputClasses = usernameInputHasError ? "form-control invalid" : "form-control"; const emailInputClasses = emailInputHasError ? "form-control invalid" : "form-control"; const passwordInputClasses = passwordInputHasError ? "form-control invalid" : "form-control"; return ( <form onSubmit={formSubmissionHandler}> <div className={usernameInputClasses}> <label htmlFor="name">Kullanıcı adı</label> <input type="text" id="name" onChange={usernameChangedHandler} onBlur={usernameBlurHandler} value={enteredUsername} /> {usernameInputHasError && ( <p className="error-text">Kullanıcı adı boş olamaz.</p> )} </div> <div className={emailInputClasses}> <label htmlFor="email">E-Mail</label> <input type="email" id="email" onChange={emailChangedHandler} onBlur={emailBlurHandler} value={enteredEmail} /> {emailInputHasError && ( <p className="error-text">Geçerli bir email adresi giriniz.</p> )} </div> <div className={passwordInputClasses}> <label htmlFor="password">Şifre</label> <input type="password" id="password" onChange={passwordChangedHandler} onBlur={passwordBlurHandler} value={enteredPassword} /> {passwordInputHasError && ( <p className="error-text">Şifre en az 7 karakter uzunluğunda olmalıdır.</p> )} </div> <div className="form-actions"> <button disabled={!formIsValid}>Gönder</button> </div> </form> ); }; export default SimpleInput;
Sonuç olarak bileşenlerimiz birçok farklı senaryoda buradaki form validasyonu örneğinde olduğu gibi benzer mantıktaki kod yapılardan oluşan gereksinimleri içerisinde barındırır. Farklı bir örnek olarak post veya get isteklerini örnek olarak verebiliriz. Bu istekler esasında aynı mantıktaki kodlardan oluşur ve bu yapılar için de generic bir yapıya ihtiyaç duyarak custom hook yapısını kullanabiliriz. Bu içerikte kaynaklar kısmında belirttiğim kurstan yararlanarak kendimce farklı bir senaryo ile oluşturduğum uygulamayı paylaştım. Bana yasinatilgan60@gmail.com adresi üzerinden ulaşabilirsiniz. İyi günler.
Kaynaklar
React – The Complete Guide (incl Hooks, React Router, Redux) – Maximilian Schwarzmüller