Özel Hook'lar ile Mantığı Tekrar Kullanma
React, useState
, useContext
, ve useEffect
gibi birkaç yerleşik Hook ile birlikte gelir. Bazen, bazı daha spesifik amaçlar için bir Hook olmasını isteyeceksiniz: örneğin, veri çekmek için, kullanıcının çevrimiçi olup olmadığını takip etmek için veya bir sohbet odasına bağlanmak için. Bu Hook’ları React’te bulamayabilirsiniz, ancak uygulamanızın ihtiyaçları için kendi Hook’larınızı oluşturabilirsiniz.
Bunları öğreneceksiniz
- Özel Hook’ların ne olduğunu ve kendi özel Hook’larınızı nasıl yazacağınızı
- Bileşenler arasında mantığı nasıl yeniden kullanacağınızı
- Özel Hook’larınızı nasıl adlandıracağınızı ve yapılandıracağınızı
- Özel Hook’ları ne zaman ve neden çıkaracağınızı
Özel Hook’lar: Bileşenler arasında mantığı paylaşma
Ağa büyük ölçüde bağımlı bir uygulama geliştirdiğinizi düşünün (çoğu uygulamanın yaptığı gibi). Kullanıcıyı, uygulamanızı kullanırken ağ bağlantısının yanlışlıkla kapandığı durumlarda uyarmak istersiniz. Bunu nasıl yapardınız? Bileşeninizde iki şeye ihtiyacınız olduğu gibi görünüyor:
- Ağınızın çevrimiçi olup olmadığını izleyen bir state parçası.
- Global
çevrimiçi
veçevrimdışı
olaylarına abone olan ve bu state’i güncelleyen bir Efekt.
Bu sizin bileşeninizi ağ durumu ile senkronize tutacaktır. Şöyle bir şeyle başlayabilirsiniz:
import { useState, useEffect } from 'react'; export default function StatusBar() { const [isOnline, setIsOnline] = useState(true); useEffect(() => { function handleOnline() { setIsOnline(true); } function handleOffline() { setIsOnline(false); } window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; }, []); return <h1>{isOnline ? '✅ Çevrimiçi' : '❌ Bağlantı kopmuş'}</h1>; }
Ağınızı kapatıp açmayı deneyin ve bu StatusBar
’ın tepki olarak nasıl güncellendiğini fark edin.
Şimdi ek olarak aynı mantığı farklı bir bileşende kullanmak istediğinizi hayal edin. Ağ kapalıyken “Kaydet” yerine “Yeniden bağlanıyor…” yazan ve devre dışı bırakılan bir Kaydet düğmesi uygulamak istiyorsunuz.
Başlangıç olarak, isOnline
state’ini ve Efekti SaveButton
’a kopyalayabilirsiniz:
import { useState, useEffect } from 'react'; export default function SaveButton() { const [isOnline, setIsOnline] = useState(true); useEffect(() => { function handleOnline() { setIsOnline(true); } function handleOffline() { setIsOnline(false); } window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; }, []); function handleSaveClick() { console.log('✅ İlerleme kaydedildi'); } return ( <button disabled={!isOnline} onClick={handleSaveClick}> {isOnline ? 'İlerlemeyi kaydet' : 'Yeniden bağlanılıyor...'} </button> ); }
Ağı kapatırsanız, düğmenin görünümünün değiştiğini doğrulayın.
Bu iki bileşen iyi çalışıyor, ancak aralarındaki mantık tekrarı talihsiz. Görünen o ki farklı görsel görünüme sahip olsalar da, aralarındaki mantığı yeniden kullanmak istiyorsunuz.
Kendi özel Hook’unuzu bir bileşenden çıkarma
Bir an için hayal edin, useState
ve useEffect
gibi, yerleşik bir useOnlineStatus
Hook’u olsaydı. O zaman bu iki bileşen de basitleştirilebilir ve aralarındaki tekrarı kaldırabilirsiniz:
function StatusBar() {
const isOnline = useOnlineStatus();
return <h1>{isOnline ? '✅ Çevrimici' : '❌ Bağlantı kopmuş'}</h1>;
}
function SaveButton() {
const isOnline = useOnlineStatus();
function handleSaveClick() {
console.log('✅ İlerleme kaydedildi');
}
return (
<button disabled={!isOnline} onClick={handleSaveClick}>
{isOnline ? 'İlerlemeyi kaydet' : 'Yeniden bağlanılıyor...'}
</button>
);
}
Yerleşik bir Hook bulunmasa da, kendiniz yazabilirsiniz. useOnlineStatus
adında bir fonksiyon oluşturun ve daha önce yazdığınız bileşenlerdeki tekrarlanan kodu içine taşıyın:
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function handleOnline() {
setIsOnline(true);
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}
Fonksiyonun sonunda isOnline
’ı döndürün. Bu, bileşenlerinizin bu değeri okumasına olanak sağlar:
import { useOnlineStatus } from './useOnlineStatus.js'; function StatusBar() { const isOnline = useOnlineStatus(); return <h1>{isOnline ? '✅ Çevrimiçi' : '❌ Bağlantı kopmuş'}</h1>; } function SaveButton() { const isOnline = useOnlineStatus(); function handleSaveClick() { console.log('✅ İlerleme kaydedildi'); } return ( <button disabled={!isOnline} onClick={handleSaveClick}> {isOnline ? 'İlerlemeyi kaydet' : 'Yeniden bağlanılıyor...'} </button> ); } export default function App() { return ( <> <SaveButton /> <StatusBar /> </> ); }
Ağı kapatıp açmanın iki bileşeni de güncellediğini doğrulayın.
Şimdi bileşenleriniz çok tekrarlı mantığa sahip değil. Daha da önemlisi, içlerindeki kod, nasıl yapacaklarından (tarayıcı olaylarına abone olarak) ziyade ne yapmak istediklerini (çevrimiçi durumu kullanın!) açıklıyor.
Mantığı özel Hook’lara çıkarttığınızda, bir harici sistem ya da tarayıcı API’si ile nasıl başa çıktığınızın zorlu ayrıntılarını gizleyebilirsiniz. Bileşenlerinizin kodu, uygulamanızın nasıl yerine getirdiğinden ziyade ne yapmak istediğinizi açıklar.
Hook isimleri her zaman use
ile başlar
React uygulamaları bileşenlerden oluşur. Bileşenler yerleşik veya özel olsun, Hook’lardan oluşur. Muhtemelen sıklıkla başkaları tarafından oluşturulan özel Hook’ları kullanacaksınız, ancak arada bir kendiniz de yazabilirsiniz!
Bu isimlendirme kurallarına uymalısınız:
- React bileşenleri büyük harfle başlamalıdır,
StatusBar
veSaveButton
gibi. React bileşenleri ayrıca, JSX gibi, React’in nasıl görüntüleyeceğini bildiği bir şey döndürmelidir. - Hook isimleri
use
ile başlayıp büyük harfle devam etmelidir,useState
(yerleşik) veyauseOnlineStatus
(özel, yukarıdaki örnekte olduğu gibi). Hook’lar keyfi değerler döndürebilir.
Bu kural, sizin bir bileşene her baktığınızda onun state, Efekt’leri ve diğer React özelliklerinin nerede “saklanabileceğini” bilmenizi garanti eder. Örneğin, bileşeninizde getColor()
fonksiyonu çağrısı görürseniz, adının use
ile başlamadığı için içinde React state’i içeremeyeceğinden emin olabilirsiniz. Ancak, useOnlineStatus()
gibi bir fonksiyon çağrısı büyük olasılıkla içinde başka Hook’lara çağrı içerecektir!
Derinlemesine İnceleme
Hayır. Hook’ları çağırmayan fonksiyonlar Hook olmak zorunda değildir.
Eğer fonksiyonunuz herhangi bir Hook çağırmıyorsa, use
ön eki kullanmayın. Bunun yerine onu use
ön eki bulunmayan bir sıradan fonksiyon olarak yazın. Örneğin, aşağıdaki useSorted
Hook çağırmadığından, onu getSorted
olarak çağırın:
// 🔴 Kaçının: Hook kullanmayan bir Hook
function useSorted(items) {
return items.slice().sort();
}
// ✅ İyi: Hook kullanmayan normal bir fonksiyon
function getSorted(items) {
return items.slice().sort();
}
Bu, kodunuzun bu sıradan fonksiyonu, koşullar dahil olmak üzere herhangi bir yerde çağırabileceğinden emin olur:
function List({ items, shouldSort }) {
let displayedItems = items;
if (shouldSort) {
// ✅ Koşullu olarak getSorted() çağırmak sorun değil çünkü o bir Hook değil
displayedItems = getSorted(items);
}
// ...
}
Bir fonksiyon eğer bir ya da daha fazla Hook’u içeriyorsa, ona use
ön eki vermelisiniz:
// ✅ İyi: Diğer Hook'ları kullanan bir Hook
function useAuth() {
return useContext(Auth);
}
Teknik olarak, bu React tarafından zorunlu kılınmıyor. Prensipte, başka Hook’ları çağırmayan bir Hook yapabilirsiniz. Bu genellikle kafa karıştırıcı ve limitleyicidir, bu yüzden bu örüntüden uzak durmak en iyisidir. Ancak, işe yarayacağı nadir durumlar bulunabilir. Örneğin: belki şu anda fonksiyonunuz hiçbir Hook kullanmıyordur, ancak gelecekte ona bazı Hook çağrıları eklemeyi planlıyorsunuzdur. O zaman, fonksiyonu use
önekiyle adlandırmak mantıklıdır:
// ✅ İyi: Gelecekte muhtemelen başka Hook'ları kullanacak bir Hook
function useAuth() {
// TODO: Authentication tamamlanınca bu satırı değiştir:
// return useContext(Auth);
return TEST_USER;
}
Bu şekilde bileşenler onu koşullu olarak çağıramayacaktır. Bu, içine Hook çağrıları eklediğinizde önemli olacaktır. Eğer onun içinde Hook kullanmayı (şimdi ya da gelecekte) planlamıyorsanız, onu bir Hook yapmayın.
Özel Hook’lar state’li mantığı paylaşmanızı sağlar, state’in kendisini değil
Daha önceki bir örnekte, ağı açıp kapattığınızda, her iki bileşen de birlikte güncellendi. Ancak, onların arasında tek bir isOnline
state değişkeninin paylaşıldığını düşünmek yanlış olur. Bu kodu inceleyin:
function StatusBar() {
const isOnline = useOnlineStatus();
// ...
}
function SaveButton() {
const isOnline = useOnlineStatus();
// ...
}
Bu, tekrarı çıkartmanızdan önceki hali gibi çalışır:
function StatusBar() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
// ...
}, []);
// ...
}
function SaveButton() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
// ...
}, []);
// ...
}
Bunlar tamamen bağımsız iki state değişkenleri ve Efekt’lerdir! Onlar rastlantısal olarak aynı anda aynı değere sahip oldular çünkü onları aynı harici değerle (ağın açık olup olmaması) senkronize ettiniz.
Bunu daha iyi canlandırabilmek adına, farklı bir örnek kullanacağız. Bu Form
bileşenini ele alın:
import { useState } from 'react'; export default function Form() { const [firstName, setFirstName] = useState('Mary'); const [lastName, setLastName] = useState('Poppins'); function handleFirstNameChange(e) { setFirstName(e.target.value); } function handleLastNameChange(e) { setLastName(e.target.value); } return ( <> <label> İsim: <input value={firstName} onChange={handleFirstNameChange} /> </label> <label> Soyisim: <input value={lastName} onChange={handleLastNameChange} /> </label> <p><b>Günaydınlar, {firstName} {lastName}.</b></p> </> ); }
Her form alanı için tekrarlayan bir mantık var:
- Bir parça state bulunuyor (
firstName
velastName
). - Bir değişim yöneticisi bulunuyor (
handleFirstNameChange
vehandleLastNameChange
). - O girdi için
value
veonChange
özniteliklerini belirleyen bir parça JSX bulunuyor.
Bu tekrarlayan mantığı useFormInput
özel Hook’una çıkartabilirsiniz:
import { useState } from 'react'; export function useFormInput(initialValue) { const [value, setValue] = useState(initialValue); function handleChange(e) { setValue(e.target.value); } const inputProps = { value: value, onChange: handleChange }; return inputProps; }
value
adında sadece bir state değişkeni oluşturduğuna dikkat edin.
Yine de, Form
bileşeni useFormInput
’u iki kez çağırıyor:
function Form() {
const firstNameProps = useFormInput('Mary');
const lastNameProps = useFormInput('Poppins');
// ...
Bu yüzden iki ayrı state değişkeni oluşturmuş gibi çalışıyor!
Özel Hook’lar sizin state’li mantık paylaşmanıza olanak sağlar, state’in kendinisini değil. Bir Hook’a yapılan her çağrı aynı Hook’a yapılan tüm çağrılardan bağımsızdır. Bu nedenle yukarıdaki iki kod alanı tamamen eşdeğerdir. İsterseniz, yukarı kayarak onları karşılaştırın. Özel bir Hook çıkartmadan önceki ve sonraki davranış tamamen aynıdır.
State’i birden fazla bileşen arasında paylaşmak istediğinizde, bunun yerine onu yukarı taşıyın ve aşağı iletin.
Hook’lar arasında reaktif değerler iletme
Özel Hook’larınızın içindeki kod, bileşeniniz her yeniden render edildiğinde yeniden yürütülecektir. Bu nedenle, bileşenler gibi, özel Hook’lar da saf olmalıdır. Özel Hook’larınızın kodunu bileşeninizin bir parçası olarak düşünün!
Özel Hook’lar bileşeninizle birlikte yeniden render edildiğinden, her zaman en son prop’ları ve state’i alırlar. Bunun ne anlama geldiğini görmek için, bu sohbet odası örneğini ele alın. Sunucu URL’sini veya sohbet odasını değiştirin:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; import { showNotification } from './notifications.js'; export default function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://localhost:1234'); useEffect(() => { const options = { serverUrl: serverUrl, roomId: roomId }; const connection = createConnection(options); connection.on('message', (msg) => { showNotification('Yeni mesaj: ' + msg); }); connection.connect(); return () => connection.disconnect(); }, [roomId, serverUrl]); return ( <> <label> Sunucu URL'i: <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} /> </label> <h1>{roomId} odasına hoşgeldiniz!</h1> </> ); }
serverUrl
ya da roomId
’yi değiştirdiğinizde, Efekt değişikliklerinize “tepki verir” ve yeniden senkronize olur. Konsol mesajlarından, Efekt’in bağlı olduğu değerleri her değiştirdiğinizde sohbetin yeniden bağlandığını görebilirsiniz.
Şimdi Efekt’in kodunu özel bir Hook’a taşıyın:
export function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
showNotification('Yeni mesaj: ' + msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]);
}
Bu ChatRoom
bileşeninizin özel Hook’unuzun içinde nasıl çalıştığıyla ilgilenmeden onu çağırmasına olanak sağlar:
export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
return (
<>
<label>
Sunucu URL'i:
<input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
</label>
<h1>{roomId} odasına hoşgeldiniz!</h1>
</>
);
}
Bu çok daha basit görünüyor! (Ama aynı şeyi yapıyor.)
Mantığın prop ve state değişikliklerine hala tepki verdiğine dikkat edin. Sunucu URL’sini veya seçilen odayı düzenlemeyi deneyin:
import { useState } from 'react'; import { useChatRoom } from './useChatRoom.js'; export default function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://localhost:1234'); useChatRoom({ roomId: roomId, serverUrl: serverUrl }); return ( <> <label> Sunucu URL'i: <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} /> </label> <h1>{roomId} odasına hoşgeldiniz!</h1> </> ); }
Bir Hook’un dönüş değerini alıp:
export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...
başka bir Hook’a girdi olarak nasıl illetiğinizi farkedin:
export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...
ChatRoom
bileşeniniz her yeniden render edildiğinde, roomId
ve serverUrl
’in son hallerini Hook’unuza verir. Bu, bir yeniden render’dan sonra değerleri her değiştikten sonra Efekt’inizin sohbete yeniden bağlanmasının nedenidir. (Eğer önceden ses ya da video işleme yazılımı ile uğraştıysanız, Hook’ları bu şekilde zincirlemek size görsel ya da ses Efektlerini zincirlemeyi hatırlatabilir. Adeta useState
’in çıktısı useChatRoom
’un girdisine “besleniyor” gibi.)
Olay yöneticilerini özel Hook’lara geçirme
useChatRoom
’u daha fazla bileşende kullanmaya başladıkça, bileşenlerin onun davranışını özelleştirmesine izin vermek isteyebilirsiniz. Örneğin, şu anda, bir mesaj geldiğinde ne yapılacağının mantığı Hook’un içine sabit kodlanmış durumda:
export function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
showNotification('Yeni mesaj: ' + msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]);
}
Diyelim ki bu mantığı bileşeninize geri taşımak istiyorsunuz:
export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useChatRoom({
roomId: roomId,
serverUrl: serverUrl,
onReceiveMessage(msg) {
showNotification('Yeni mesaj: ' + msg);
}
});
// ...
Bunun çalışmasını sağlamak için, özel Hook’unuzu adlandırılmış seçeneklerinden biri olarak onReceiveMessage
’ı alacak şekilde değiştirin:
export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
onReceiveMessage(msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl, onReceiveMessage]); // ✅ Tüm bağımlılıklar bildirildi
}
Bu çalışacaktır, ancak özel Hook’unuz olay yöneticilerini kabul ediyorsa yapabileceğiniz bir geliştirme daha var.
onReceiveMessage
’a bir bağımlılık eklemek ideal değildir çünkü bileşen her yeniden render edildiğinde sohbetin yeniden bağlanmasına neden olacaktır. Bu olay yöneticisini bağımlılıklardan çıkartmak için bir Efekt Olayı’na sarın:
import { useEffect, useEffectEvent } from 'react';
// ...
export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
const onMessage = useEffectEvent(onReceiveMessage);
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
onMessage(msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]); // ✅ Tüm bağımlılıklar bildirildi
}
Şimdi sohbet, ChatRoom
bileşeni her yeniden render edildiğinde yeniden bağlanmayacaktır. Burada özel bir Hook’a bir olay yöneticisi iletmekle ilgili oynayabileceğiniz tamamen çalışan bir demo var:
import { useState } from 'react'; import { useChatRoom } from './useChatRoom.js'; import { showNotification } from './notifications.js'; export default function ChatRoom({ roomId }) { const [serverUrl, setServerUrl] = useState('https://localhost:1234'); useChatRoom({ roomId: roomId, serverUrl: serverUrl, onReceiveMessage(msg) { showNotification('Yeni mesaj: ' + msg); } }); return ( <> <label> Sunucu URL'i: <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} /> </label> <h1>{roomId} odasına hoşgeldiniz!</h1> </> ); }
useChatRoom
’un nasıl çalıştığını artık bilmenize gerek olmadığını farkedin. Onu herhangi bir başka bileşene ekleyebilir, herhangi başka seçenekler iletebilirsiniz, aynı şekilde çalışacaktır. Bu özel Hook’ların gücüdür.
Özel Hook’lar ne zaman kullanılmalıdır
Her ufak tekrarlanan kod parçası için bir özel Hook çıkarmanıza gerek yok. Bazı tekrarlanmalar sorun değildir. Örneğin, yukarıdaki gibi tek bir useState
çağrısını saran bir useFormInput
Hook’u çıkartmak muhtemelen gereksizdir.
Ancak, her Efekt yazdığınızda, onu özel bir Hook’a sarmanın daha net olup olmayacağını düşünün. Efekt’lere çok sık ihtiyacınız olmamalı yani eğer bir Efekt yazıyorsanız, bu “React’ten dışarı çıkmak” ve bazı harici sistemlerle senkronize olmanız ya da React’in dahili bir API’sinin sağlamadığı bir şeyi yapmanız gerektiği anlamına gelir. Onu özel bir Hook’a sararak, niyetinizi ve verinin onun içinden nasıl aktığına dair bilgiyi net bir şekilde iletebilirsiniz.
Örneğin, iki açılır menü bileşenini gösteren bir ShippingForm
bileşenini ele alın: birisi şehirlerin listesini, diğeri seçilen şehirdeki alanların listesini göstersin. Şöyle bir kodla başlayabilirsiniz:
function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
// Bu Efekt bir ülke için şehirleri çeker
useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]);
const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);
// Bu Efekt seçilen şehir için alanları çeker
useEffect(() => {
if (city) {
let ignore = false;
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
return () => {
ignore = true;
};
}
}, [city]);
// ...
Bu kod biraz tekrarlayıcı olsa da, bu Efekt’leri birbirinden ayrı tutmak doğrudur. İki farklı şeyi senkronize ediyorlar, bu yüzden onları tek bir Efekt’e birleştirmemelisiniz. Bunun yerine, yukarıdaki ShippingForm
bileşenini aralarındaki ortak mantığı kendi useData
Hook’unuza çıkartarak basitleştirebilirsiniz:
function useData(url) {
const [data, setData] = useState(null);
useEffect(() => {
if (url) {
let ignore = false;
fetch(url)
.then(response => response.json())
.then(json => {
if (!ignore) {
setData(json);
}
});
return () => {
ignore = true;
};
}
}, [url]);
return data;
}
Şimdi ShippingForm
içindeki her iki Efekt’i de useData
’nın çağrılarıyla değiştirebilirsiniz:
function ShippingForm({ country }) {
const cities = useData(`/api/cities?country=${country}`);
const [city, setCity] = useState(null);
const areas = useData(city ? `/api/areas?city=${city}` : null);
// ...
Bir özel Hook çıkarmak veri akışını aşikâr hale getirir. url
’i içeri beslersiniz ve data
’yı dışarı alırsınız. Efekt’inizi useData
’nın içine “gizleyerek”, ShippingForm
bileşeninde çalışan birinin ona gereksiz bağımlılıklar eklemesini de engellersiniz. Zamanla, uygulamanızın çoğu Efekti özel Hook’larda olacaktır.
Derinlemesine İnceleme
Özel Hook’unuzun adını seçerek başlayın. Eğer net bir isim seçmekte zorlanıyorsanız, bu Efek’inizin bileşeninizin geri kalan mantığına çok bağlı olduğu ve henüz çıkartılmaya hazır olmadığı anlamına gelebilir.
İdeal olarak, özel Hook’unuzun adı kod yazmayan bir kişinin bile ne yaptığını, ne aldığını ve ne döndürdüğünü tahmin edebileceği kadar açık olmalıdır:
- ✅
useData(url)
- ✅
useImpressionLog(eventName, extraData)
- ✅
useChatRoom(options)
Dış bir sistemle senkronize olduğunuzda, özel Hook adınız daha teknik olabilir ve o sisteme özel jargon kullanabilir. Bu, o sisteme aşina bir kişi için açık olduğu sürece sorun değildir:
- ✅
useMediaQuery(query)
- ✅
useSocket(url)
- ✅
useIntersectionObserver(ref, options)
Özel Hook’ların somut üst düzey kullanım durumlarına odaklanmasını sağlayın. UseEffect` API’sinin kendisi için alternatif ve kolaylık sağlayan sarmalayıcılar olarak hareket eden özel “yaşam döngüsü” Hook’ları oluşturmaktan ve kullanmaktan kaçının:
Özel Hook’larınızı somut yüksek seviyeli kullanım durumlarına odaklı tutun. useEffect
API’sinin kendisi için alternatifler ve kolaylık sarıcıları olan özel “lifecycle” Hook’ları oluşturmayın ve kullanmayın:
- 🔴
useMount(fn)
- 🔴
useEffectOnce(fn)
- 🔴
useUpdateEffect(fn)
Örneğin, bu useMount
Hook’u bazı kodun sadece “mount” sırasında çalışmasını sağlamaya çalışır:
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
// 🔴 Kaçının: özel "lifecycle" Hook'ları kullanmak
useMount(() => {
const connection = createConnection({ roomId, serverUrl });
connection.connect();
post('/analytics/event', { eventName: 'visit_chat' });
});
// ...
}
// 🔴 Kaçının: özel "lifecycle" Hook'ları oluşturmak
function useMount(fn) {
useEffect(() => {
fn();
}, []); // 🔴 React Hook'u useEffect'in bir bağımlılığı eksik: 'fn'
}
useMount
gibi özel “lifecycle” Hook’ları React paradigmasına pek iyi uymaz. Örneğin, bu kod örneğinde bir hata var (roomId
ya da serverUrl
’deki değişikliklere “tepki” vermiyor), ama linter sizi bunun hakkında uyarmayacaktır, çünkü linter sadece doğrudan useEffect
çağrılarını kontrol eder. Hook’unuz hakkında bilgisi olmayacaktır.
Eğer bir Efekt yazıyorsanız, React API’sini doğrudan kullanarak başlayın:
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
// ✅ İyi: amaçlarına göre ayrılmış iki saf Efekt
useEffect(() => {
const connection = createConnection({ serverUrl, roomId });
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]);
useEffect(() => {
post('/analytics/event', { eventName: 'visit_chat', roomId });
}, [roomId]);
// ...
}
Sonra, farklı yüksek seviyeli kullanım durumları için özel Hook’lar çıkartabilirsiniz (ama çıkartmak zorunda değilsiniz):
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
// ✅ İyi: amaçlarına göre adlandırılmış özel Hook'lar
useChatRoom({ serverUrl, roomId });
useImpressionLog('visit_chat', { roomId });
// ...
}
İyi bir özel Hook çağıran kodun ne yaptığını sınırlandırarak daha bildirimsel bir hale getirir. Örneğin, useChatRoom(options)
sadece sohbet odasına bağlanabilirken, useImpressionLog(eventName, extraData)
analitiklere bir izlenim kaydı gönderebilir. Eğer özel Hook API’nız kullanım durumlarını sınırlandırmıyorsa ve çok soyutsa, uzun vadede çözdüğünden daha fazla sorun yaratabilir.
Özel Hook’lar daha iyi kalıplara geçiş yapmanıza yardımcı olur
Efekt’ler bir “kaçış yolu“‘dur: Efekt’leri “React’ten dışarı çıkmak” zorunda kaldığınızda ve daha iyi bir yerleşik çözüm olmadığında kullanırsınız. Zamanla, React ekibinin amacı daha spesifik problemlere daha spesifik çözümler sağlayarak uygulamanızdaki Efekt’lerin sayısını minimuma indirmektir. Efekt’lerinizi özel Hook’larla sarmak, bu çözümler mevcut olduğunda kodunuzu güncellemeyi kolaylaştırır.
Şu örneğe geri dönelim:
import { useState, useEffect } from 'react'; export function useOnlineStatus() { const [isOnline, setIsOnline] = useState(true); useEffect(() => { function handleOnline() { setIsOnline(true); } function handleOffline() { setIsOnline(false); } window.addEventListener('online', handleOnline); window.addEventListener('offline', handleOffline); return () => { window.removeEventListener('online', handleOnline); window.removeEventListener('offline', handleOffline); }; }, []); return isOnline; }
Yukarıdaki örnekte, useOnlineStatus
, useState
ve useEffect
. ikilisi kullanılarak oluşturulmuştur. Ancak, bu en iyi muhtemel çözüm değildir. Dikkate alınmayan birçok uç senaryo vardır. Örneğin, bileşen DOM’a eklendiğinde, isOnline
’ın halihazırda true
olacağını varsayar, ancak ağ halihazırda çevrimdışı olduğunda bu yanlış olabilir. Bu durumu kontrol etmek için tarayıcının navigator.onLine
API’sini kullanabilirsiniz, ancak bunu doğrudan kullanmak ilk HTML’i sunucuda oluşturmak için çalışmayacaktır. Kısacası, bu kod geliştirilebilir.
Şansımıza, React 18 bu sorunların hepsini sizin için çözecek olan useSyncExternalStore
adında özel bir API içerir. İşte bu yeni API’den faydalanarak yeniden yazılmış useOnlineStatus
Hook’unuz:
import { useSyncExternalStore } from 'react'; function subscribe(callback) { window.addEventListener('online', callback); window.addEventListener('offline', callback); return () => { window.removeEventListener('online', callback); window.removeEventListener('offline', callback); }; } export function useOnlineStatus() { return useSyncExternalStore( subscribe, () => navigator.onLine, // İstemci tarafında değerin ne olacağı () => true // Sunucu tarafında değerin ne olacağı ); }
Bu değişikliği yapmak için herhangi bir bileşeni değiştirmeye ihtiyacınız olmadığını farkedin:
function StatusBar() {
const isOnline = useOnlineStatus();
// ...
}
function SaveButton() {
const isOnline = useOnlineStatus();
// ...
}
Efektleri özel hook’lara sarmanın genellikle faydalı olmasının başka bir nedeni budur:
- Efektlerinizin içine ve Efekt’lerinizden dışarı akan veriyi oldukça belirgin hale getirirsiniz.
- Bileşenlerinizin, Efektlerinizin nasıl çalıştığından ziyade ne yapmak istediğine odaklanmasını sağlarsınız.
- React yeni özellikler eklediğinde, bu Efekt’leri bileşenlerinizde herhangi bir değişiklik yapmadan kaldırabilirsiniz.
Bir tasarım sistemine benzer olarak, uygulamanızdaki bileşenlerde bulunan ortak kalıpları özel hook’lara çıkartmaya başlamayı faydalı bulabilirsiniz. Bu, bileşenlerinizin kodunu niyete odaklı tutar ve sık sık ham Efektler yazmaktan kaçınmanızı sağlar. Pek çok muazzam özel hook’lar React topluluğu tarafından sürdürülmektedir.
Derinlemesine İnceleme
Detaylar üzerine çalışmaya devam ediyoruz, ancak gelecekte veri getirmeyi şu şekilde yazmanızı bekliyoruz:
import { use } from 'react'; // Henüz mevcut değil!
function ShippingForm({ country }) {
const cities = use(fetch(`/api/cities?country=${country}`));
const [city, setCity] = useState(null);
const areas = city ? use(fetch(`/api/areas?city=${city}`)) : null;
// ...
Eğer useData
gibi özel hookları uygulamanızda kullanıyorsanız, neticede önerilen yaklaşıma geçiş yapmak için her bileşende manuel olarak ham Efektler yazılan bir yaklaşıma göre daha az değişiklik gerekecektir. Ancak, eski yaklaşım hala sorunsuz çalışacaktır, yani ham Efektler yazmaktan mutluysanız, bunu yapmaya devam edebilirsiniz.
Yapmanın birden fazla yolu vardır
Diyelim ki tarayıcı requestAnimationFrame
API’sini kullanarak sıfırdan bir fade-in animasyonu yapmak istiyorsunuz. Bir animasyon döngüsü kuracak bir Efekt ile başlayabilirsiniz. Animasyonun her bir karesinde, bir ref’te tuttuğunuz DOM node’unun opaklığını 1
olana kadar değiştirebilirsiniz. Kodunuz şöyle başlayabilir:
import { useState, useEffect, useRef } from 'react'; function Welcome() { const ref = useRef(null); useEffect(() => { const duration = 1000; const node = ref.current; let startTime = performance.now(); let frameId = null; function onFrame(now) { const timePassed = now - startTime; const progress = Math.min(timePassed / duration, 1); onProgress(progress); if (progress < 1) { // Hala boyama yapılacak kare var frameId = requestAnimationFrame(onFrame); } } function onProgress(progress) { node.style.opacity = progress; } function start() { onProgress(0); startTime = performance.now(); frameId = requestAnimationFrame(onFrame); } function stop() { cancelAnimationFrame(frameId); startTime = null; frameId = null; } start(); return () => stop(); }, []); return ( <h1 className="welcome" ref={ref}> Hoşgeldiniz </h1> ); } export default function App() { const [show, setShow] = useState(false); return ( <> <button onClick={() => setShow(!show)}> {show ? 'Kaldır' : 'Göster'} </button> <hr /> {show && <Welcome />} </> ); }
Bileşeni daha okunabilir yapmak adına, mantığı useFadeIn
adında özel bir Hook’a çıkarabilirsiniz:
import { useState, useEffect, useRef } from 'react'; import { useFadeIn } from './useFadeIn.js'; function Welcome() { const ref = useRef(null); useFadeIn(ref, 1000); return ( <h1 className="welcome" ref={ref}> Hoşgeldiniz </h1> ); } export default function App() { const [show, setShow] = useState(false); return ( <> <button onClick={() => setShow(!show)}> {show ? 'Kaldır' : 'Göster'} </button> <hr /> {show && <Welcome />} </> ); }
useFadeIn
kodunu olduğu gibi bırakabilirsiniz, ancak daha fazla refaktör yapabilirsiniz. Örneğin, animasyon döngüsünü kurma mantığını useFadeIn
’den çıkarıp özel bir useAnimationLoop
Hook’a çıkarabilirsiniz:
import { useState, useEffect } from 'react'; import { experimental_useEffectEvent as useEffectEvent } from 'react'; export function useFadeIn(ref, duration) { const [isRunning, setIsRunning] = useState(true); useAnimationLoop(isRunning, (timePassed) => { const progress = Math.min(timePassed / duration, 1); ref.current.style.opacity = progress; if (progress === 1) { setIsRunning(false); } }); } function useAnimationLoop(isRunning, drawFrame) { const onFrame = useEffectEvent(drawFrame); useEffect(() => { if (!isRunning) { return; } const startTime = performance.now(); let frameId = null; function tick(now) { const timePassed = now - startTime; onFrame(timePassed); frameId = requestAnimationFrame(tick); } tick(); return () => cancelAnimationFrame(frameId); }, [isRunning]); }
Ancak, bunu yapmak zorunda değilsiniz. Normal fonksiyonlarda olduğu gibi, kodunuzun farklı bölümleri arasındaki sınırların nerede çizileceğine nihayetinde siz karar verirsiniz.. Çok farklı bir yaklaşım da benimseyebilirsiniz. Mantığı Efekt içinde tutmak yerine, zorunlu mantığın çoğunu bir JavaScript sınıf: içine taşıyabilirsiniz.
Ancak, bunu yapmak zorunda değildiniz. Normal fonksiyonlarda olduğu gibi, sonuçta kodunuzun farklı parçaları arasındaki sınırları nerede çizeceğinize siz karar verirsiniz. Çok farklı bir yaklaşım da seçebilirsiniz. Mantığı Efekt’te tutmak yerine, mantığın büyük bir kısmını bir JavaScript sınıfına taşıyabilirsiniz:
import { useState, useEffect } from 'react'; import { FadeInAnimation } from './animation.js'; export function useFadeIn(ref, duration) { useEffect(() => { const animation = new FadeInAnimation(ref.current); animation.start(duration); return () => { animation.stop(); }; }, [ref, duration]); }
Efekt’ler, React’i dış sistemlere bağlamanıza olanak tanır. Daha fazla Efekt koordinasyonu gerektiğinde (örneğin, birden fazla animasyonu zincirlemek için), yukarıdaki örnekte olduğu gibi mantığı Efekt’lerden ve Hook’lardan tamamen çıkarmanız daha mantıklı hale gelir. Ardından, çıkarttığınız kod “dış sistem” haline gelir. Bu, Efekt’lerinizin sade kalmasını sağlar çünkü sadece React dışına taşıdığınız sisteme mesaj göndermeleri gerekir.
Yukarıdaki örnekler fade-in mantığının JavaScript’te yapılması gerektiğini varsayar. Ancak, bu özel fade-in animasyonunu düz CSS Animasyonu ile uygulamak hem daha basit hem de çok daha verimlidir:
.welcome { color: white; padding: 50px; text-align: center; font-size: 50px; background-image: radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%); animation: fadeIn 1000ms; } @keyframes fadeIn { 0% { opacity: 0; } 100% { opacity: 1; } }
Bazen, bir Hook’a bile ihtiyacınız olmayabilir!
Özet
- Özel Hook’lar, bileşenler arasında mantığı paylaşmanıza olanak tanır.
- Özel Hook’ların isimleri
use
ile başlamalı ve bir büyük harfle devam etmelidir. - Özel Hook’lar sadece state’li mantığı paylaşır, state’in kendisini değil.
- Reaktif değerleri bir Hook’tan diğerine paslayabilirsiniz ve bunlar güncel kalırlar.
- Tüm Hook’lar, bileşeniniz yeniden renderlandığında her zaman yeniden çalışır.
- Özel Hook’larınızın kodu, bileşeninizin kodu gibi saf olmalıdır.
- Özel Hook’lar tarafından alınan olay yöneticilerini Efekt olaylarına sarın.
useMount
gibi özel Hook’lar oluşturmayın. Amaçlarınızı belirli tutun.- Kodunuzun sınırlarını nasıl ve nerede çizeceğinize siz karar verirsiniz.
Problem 1 / 5: Bir useCounter
Hook’u çıkarın
Bu bileşen bir state değişkeni ve bir Efekt kullanarak her saniye artan bir sayıyı görüntüler. Bu mantığı useCounter
adında özel bir Hook’a çıkarın. Amacınız Counter
bileşeninin uygulamasını tam olarak aşağıdaki gibi yapmak olmalıdır:
export default function Counter() {
const count = useCounter();
return <h1>Geçen saniyeler: {count}</h1>;
}
Özel Hook’unuzu useCounter.js
’e yazmanız ve onu App.js
dosyasına aktarmanız gerekecek.
import { useState, useEffect } from 'react'; export default function Counter() { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => { setCount(c => c + 1); }, 1000); return () => clearInterval(id); }, []); return <h1>Geçen saniyeler: {count}</h1>; }