Efektler ile Senkronize Etme
Bazı bileşenler harici sistemler ile senkronize olmalıdır. Örneğin, React state’ine göre React olmayan bir bileşeni kontrol etmek, bir sunucu bağlantısı kurmak veya bir bileşen ekranda göründüğünde analiz bilgisi göndermek isteyebilirsiniz. Efektler, bileşeninizi bazı React dışı sistemler ile senkronize etmenizi sağlamak için bazı kodları render işleminden sonra çalıştırır.
Bunları öğreneceksiniz
- Efektler nelerdir
- Efektler olaylardan nasıl farklıdır
- Bileşeninizde nasıl Efekt bildirirsiniz
- Efekti gereksiz olarak yeniden çalıştırmaktan nasıl kaçınırsınız
- Efektler geliştirme sırasında neden iki kere çalışır ve bunu nasıl düzeltiriz
Efektler nedir ve olaylardan nasıl farklıdırlar?
Efektlere başlamadan önce React bileşenleri içindeki iki tip mantığa aşina olmalısınız:
-
Kodun render edilmesi (Describing the UI başlığında bahsedildi) bileşeninizin en üst seviyesindedir. Burası prop’ları ve state’i aldığınız, onları dönüştürdüğünüz ve ekranda görmek istediğiniz JSX’i döndürdüğünüz yerdir. Kodu render etmek saf olmak zorundadır. Bir matematik formülünde olduğu gibi sadece sonucu hesaplamalı ve başka hiçbir şey yapmamalıdır.
-
Olay yöneticileri (Adding Interactivity başlığında bahsedildi) bileşeniniz içinde sadece hesaplama yapmak yerine bir şeyler yapan iç içe fonksiyonlardır. Bir olay yöneticisi input alanını güncelleyebilir, bir ürünü satılan almak için HTTP POST isteği gönderebilir ya da kullanıcıyı başka bir ekrana yönlendirebilir. Olay yöneticileri, belirli bir kullanıcı eyleminin (örneğin bir butana tıklamak ya da yazmak) “yan etkiler” içerirler (programın state’ini değiştirirler).
Bazen bu yeterli değildir. Ekranda göründüğünde sohbet sunucusuna bağlanmak zorunda olan bir ChatRoom
bileşenini ele alalım. Bir sunucuya bağlanmak saf bir hesaplama değildir (bir yan etkidir) bu yüzden render etme sırasında yapılamaz. Ancak, ChatRoom
’un ekranda görüntülenmesine neden olan tıklama gibi belirli bir olay yoktur.
Efektler, belirli bir olaydan ziyade render etmenin kendisinden kaynaklanan yan etkileri belirtmenizi sağlar. Sohbete mesaj göndermek bir olaydır çünkü direkt olarak kullanıcının belirli bir butona tıklaması ile gerçekleşir. Ancak, bir sunucu bağlantısı kurmak Efekttir çünkü bileşenin ekranda görünmesine neden olan etkileşim ne olursa olsun gerçekleşmesi gerekir. Efektler ekran güncellendikten sonra işlemenin (commit) sonunda çalışır. Bu, React bileşenlerini bazı harici sistemlerle (ağ veya üçüncü parti kütüphane ile) senkronize etmek için iyi bir zamandır.
Bir Efekte ihtiyacınız olmayabilir
Bileşenlerinize Efekt eklemekte acele etmeyin. Efektlerin genel olarak React kodununuzun “dışına çıkmakta” ve bazı harici sistemler ile senkronize etmekte kullanıldığı aklınızda olsun. Bu tarayıcı API’larını, üçüncü parti widget’ları, ağları ve diğer şeyleri içerir. Efektiniz yalnızca bazı state’leri başka bir state’e göre ayarlıyorsa, bir Efekte ihtiyacınız olmayabilir.
Efekt nasıl yazılır
Bir Efekt yazmak için aşağıdaki üç adımı takip edin:
- Bir Efekt bildirin. Varsayılan olarak, Efektiniz her işleme’den sonra çalışacaktır.
- Efektin bağımlılıklarını belirtin. Çoğu Efekt her render yerine yalnızca gerektiğinde yeniden çalışmalıdır. Örneğin, bir solma animasyonu yalnızca bileşen göründüğünde tetiklenmelidir. Bir sohbet odasına bağlanmak ya da bağlantıyı koparmak yalnızca bileşen göründüğünde ve kaybolduğunda ya da sohbet odası değiştiğinde olmalıdır. Bağımlılıkları belirterek bunu nasıl kontrol edeceğinizi öğreneceksiniz.
- Gerekliyse temizleme (cleanup) ekleyin. Bazı Efektlerin, yaptıkları her şeyi nasıl durduracaklarını, geri alacaklarını veya temizleyeceklerini belirtmeleri gerekir. Örneğin, “bağlanmak” “bağlantıyı kese” ihtiyaç duyar, “abone ol” “abonelikten çıka” ihtiyaç duyar ve “veri getirme (fetch)” ya “iptal” ya da “görmezden gele” ihtiyaç duyar. Bir temizleme fonksiyonu döndürerek bunu nasıl yapacağınızı öğreneceksiniz.
Gelin her bir adıma detaylı bir şekilde bakalım.
1. Adım: Bir Efekt bildirin
Bileşeninizde bir Efekt bildirmek için useEffect
Hook’unu içe aktarın:
import { useEffect } from 'react';
Daha sonra, bileşeninizin üst seviyesinde çağırın ve Efektiniz içine kodunuzu yazın:
function MyComponent() {
useEffect(() => {
// Buradaki kod *her* render'dan sonra çalışır
});
return <div />;
}
Bileşeniniz her render edildiğinde, React ekranı güncelleyecektir ve sonra useEffect
içindeki kodu çalıştıracaktır. Diğer bir deyişle, useEffect
bir kod parçasının çalışmasını o render işlemi ekrana yansıtılana kadar “geciktirir”.
Harici bir sistemle senkronize etmek için Efekti nasıl kullanacağımızı görelim. Bir <VideoPlayer>
React bileşeni düşünün. Bu bileşene isPlaying
prop’u ileterek videonun çalıyor mu yoksa duraklatılmış mı olduğunu kontrol etmek güzel olurdu:
<VideoPlayer isPlaying={isPlaying} />;
Özel VideoPlayer
bileşeniniz tarayıcıya yerleşik <video>
elemanını render eder:
function VideoPlayer({ src, isPlaying }) {
// YAPILACAK: isPlayinga ile bir şey yap
return <video src={src} />;
}
Ancak, tarayıcı <video>
elemanı isPlaying
prop’una sahip değildir. Kontrol etmenin tek yolu manuel olarak DOM elemanında play()
ve pause()
metodlarını çağırmaktır. Videonun şu anda oynatılıp oynatılmayacağını söyleyen isPlaying
prop’unun değerini play()
ve pause()
gibi çağrıları kullanarak senkronize etmeniz gerekmektedir.
İlk olarak <video>
DOM node’una bir ref verin.
Render etme sırasında play()
veya pause()
metodlarını çağırmak isteyebilirsiniz ancak bu doğru bir yöntem değil:
import { useState, useRef, useEffect } from 'react'; function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); if (isPlaying) { ref.current.play(); // Render etme sırasında bunları çağırmaya izin verilmez. } else { ref.current.pause(); // Ayrıca bu çöker } return <video ref={ref} src={src} loop playsInline />; } export default function App() { const [isPlaying, setIsPlaying] = useState(false); return ( <> <button onClick={() => setIsPlaying(!isPlaying)}> {isPlaying ? 'Duraklat' : 'Oynat'} </button> <VideoPlayer isPlaying={isPlaying} src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" /> </> ); }
Bu kodun doğru olmamasının sebebi render etme esnasında DOM node’una bir şeyler yapmaya çalışmasıdır. React’te, render etme JSX’ın saf bir hesaplaması olmalıdır ve DOM’u değiştirme gibi yan etkileri içermemelidir.
Dahası, VideoPlayer
ilk çağırıldığı zaman, o bileşen henüz DOM’da değildir! Henüz play()
ve pause()
metodları çağırılıcak bir DOM node’u yok çünkü React, siz JSX’i döndürene kadar hangi DOM’u oluşturacağını bilmiyor.
Buradaki çözüm, yan etkiyi render etme hesaplamasının dışına çıkarmak için useEffect içine taşımaktır:
import { useEffect, useRef } from 'react';
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
});
return <video ref={ref} src={src} loop playsInline />;
}
DOM güncellemesini Efekt içine taşıyarak React’in ilk olarak ekranı güncellemesine izin verirsiniz. Ardından Efektiniz çalışır.
VideoPlayer
bileşeniniz render edildiğinde (ya ilk sefer ya da yeniden render’larda), birkaç şey olacaktır. İlk olarak, React ekranı güncelleyecektir, bu <video>
elemanının DOM’da doğru prop’larla olmasını sağlayacaktır. Ardından React, Efektinizi çalıştıracaktır. Son olarak, Efektiniz isPlaying
değerine göre play()
veya pause()
metodlarını çağıracaktır.
Oynat/Duraklat butonuna birkaç kez basın ve video oynatıcının isPlaying
değeriyle nasıl senkronize kaldığını görün:
import { useState, useRef, useEffect } from 'react'; function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); useEffect(() => { if (isPlaying) { ref.current.play(); } else { ref.current.pause(); } }); return <video ref={ref} src={src} loop playsInline />; } export default function App() { const [isPlaying, setIsPlaying] = useState(false); return ( <> <button onClick={() => setIsPlaying(!isPlaying)}> {isPlaying ? 'Duraklat' : 'Oynat'} </button> <VideoPlayer isPlaying={isPlaying} src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" /> </> ); }
Bu örnekte, React state’ine senkronize ettiğiniz “harici sistem” tarayıcı medya API’ıdır. Eski, React olmayan kodu (JQuery eklentileri gibi) bildirim temelli React bileşenlerine sarmak için benzer bir yaklaşım kullanabilirsiniz.
Bir video oynatıcısını kontrol etmenin pratikte çok daha kompleks bir şey olduğuna dikkat edin. play()
metodunu çağırmak başarısız olabilir, kullanıcı tarayıcıya yerleşik kontrolleri kullanarak videoyu oynatıp durdurabilir ve bunun gibi şeyler. Bu örnek çok basitleştirilmiş ve eksiktir.
2. Adım: Efekt bağımlılıklarını belirtin
Varsayılan olarak, Efektler her render’dan sonra çalışır. Genellikle bu, istediğiniz davranış değildir:
- Bazen yavaştır. Harici bir sistem ile senkronize etmek anlık bir olay değildir bu yüzden gerekli olmadıkça senkronize etme işlemini atlamak isteyebilirsiniz. Örneğin, her bir tuşa her bastığınızda sohbet sunucusuna tekrar bağlanmak istemezsiniz.
- Bazen yanlıştır. Örneğin, her bir tuşa her bastığınızda bileşen solma animasyonunu tetiklemek istemezsiniz. Animasyon yalnızca bir kere, bileşen ekranda belirdiği zaman tetiklenmelidir.
Sorunu göstermek için, birkaç console.log
çağrısı ve üst bileşenin state’ini güncelleyen bir input alanı içeren önceki örneği burada bulabilirsiniz. Yazmanın Efektin nasıl yeniden çalışmasına neden olduğuna dikkat edin:
import { useState, useRef, useEffect } from 'react'; function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); useEffect(() => { if (isPlaying) { console.log('video.play() çağrılıyor'); ref.current.play(); } else { console.log('video.pause() çağrılıyor'); ref.current.pause(); } }); return <video ref={ref} src={src} loop playsInline />; } export default function App() { const [isPlaying, setIsPlaying] = useState(false); const [text, setText] = useState(''); return ( <> <input value={text} onChange={e => setText(e.target.value)} /> <button onClick={() => setIsPlaying(!isPlaying)}> {isPlaying ? 'Duraklat' : 'Oynat'} </button> <VideoPlayer isPlaying={isPlaying} src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" /> </> ); }
useEffect
çağrısının ikinci argümanı olarak bağımlılıklar dizisini belirterek React’e Efekti gereksiz yere yeniden çalıştırmayı atlamasını söyleyebilirsiniz. Yukarıdaki örnekteki 14. satıra boş bir []
dizi ekleyin:
useEffect(() => {
// ...
}, []);
React Hook'u useEffect'in eksik bir bağımlılığı var: 'isPlaying'
diyen bir hata göreceksiniz:
import { useState, useRef, useEffect } from 'react'; function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); useEffect(() => { if (isPlaying) { console.log('video.play() çağrılıyor'); ref.current.play(); } else { console.log('video.pause() çağrılıyor'); ref.current.pause(); } }, []); // Bu bir hataya neden olur return <video ref={ref} src={src} loop playsInline />; } export default function App() { const [isPlaying, setIsPlaying] = useState(false); const [text, setText] = useState(''); return ( <> <input value={text} onChange={e => setText(e.target.value)} /> <button onClick={() => setIsPlaying(!isPlaying)}> {isPlaying ? 'Duraklat' : 'Oynat'} </button> <VideoPlayer isPlaying={isPlaying} src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" /> </> ); }
Buradaki problem Efektiniz içindeki kodun ne yapacağı isPlaying
prop’una bağlıdır ancak bu bağımlılık açıkça bildirilmemiştir. Bu sorunu düzeltmek için bağımlılık dizisine isPlaying
ekleyin:
useEffect(() => {
if (isPlaying) { // Burada kullanılıyor...
// ...
} else {
// ...
}
}, [isPlaying]); // ...yani burada bildirilmeli!
Şimdi bütün bağımlılıklar bildirildiği için artık hata mesajı yoktur. [isPlaying]
’i bağımlılık listesinde belirtmek React’in, eğer isPlaying
değeri bir önceki render ile aynıysa Efektinizin yeniden çalışmasını atlamasını sağlar. Bu değişiklikle beraber input alanına bir şey yazmak Efektin yeniden çalışmasına neden olmaz ama Oynat/Duraklat butonuna basmak Efekti yeniden çalıştırır:
import { useState, useRef, useEffect } from 'react'; function VideoPlayer({ src, isPlaying }) { const ref = useRef(null); useEffect(() => { if (isPlaying) { console.log('video.play() çağrılıyor'); ref.current.play(); } else { console.log('video.pause() çağrılıyor'); ref.current.pause(); } }, [isPlaying]); return <video ref={ref} src={src} loop playsInline />; } export default function App() { const [isPlaying, setIsPlaying] = useState(false); const [text, setText] = useState(''); return ( <> <input value={text} onChange={e => setText(e.target.value)} /> <button onClick={() => setIsPlaying(!isPlaying)}> {isPlaying ? 'Duraklat' : 'Oynat'} </button> <VideoPlayer isPlaying={isPlaying} src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4" /> </> ); }
Bağımlılık dizisi birden çok bağımlılık içerebilir. React, yalnızca belirttiğiniz bağımlılıkların tümünün önceki render sırasına sahip oldukları değerlerle tamamen aynı değerlere sahip olması durumunda Efekti çalıştırmayı atlayacaktır. React, bağımlılıkların değerlerini Object.is
kıyaslamasını kullanarak karşılaştırır. Detaylar için useEffect
referansına göz atın.
Bağımlılıklarınızı “seçemediğinize” dikkat edin. Eğer belirttiğiniz bağımlılıklar React’in Efektiniz içindeki kodunuza göre beklediği bağımlılıklar ile eşleşmiyorsa bir lint hatası alacaksınız. Bu kodunuzdaki pek çok hatayı yakalamanıza yardımcı olur. Eğer bir kodun tekrar çalışmasını istemiyorsanız, Efekt kodunun kendisini o bağımlılığa “ihtiyacı olmayacak” şekilde düzenleyin.
Derinlemesine İnceleme
Bu Efekt hem ref
hem de isPlaying
kullanmaktadır ancak yalnızca isPlaying
bağımlılık olarak bildirilmiştir:
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
}, [isPlaying]);
Bunun nedeni ref
nesnesi sabit bir kimliğe sahiptir: React, her render’da aynı useRef
çağrısından her zaman aynı nesneyi alacağınızı garanti eder. Nesne hiçbir zaman değişmez bu yüzden nesne Efektin yeniden çalışmasına neden olmaz. Bu yüzden bağımlılık listesine dahil edip etmediğiniz önemli değildir. Dahil etmek de sorun yaratmaz:
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
}, [isPlaying, ref]);
useState
’den döndürülen set
fonksiyonları da sabit bir kimliğe sahiptir bu yüzden bağımlılık dizilerine dahil edilmediklerini sık sık görürsünüz. Eğer linter bir bağımlılığı dahil etmediğinizde hata vermiyorsa, dahil etmemeniz güvenlidir.
Sürekli-sabit olan bağımlılıkları dahil etmemek yalnızca linter nesnenin sabit olduğunu “görebildiğinde” çalışır. Örneğin, eğer ref
üst bir elemandan iletiliyorsa, onu bağımlılık dizisinde belirtmeniz gerekmektedir. Ancak bu iyidir çünkü üst bileşenin her zaman aynı ref’i mi yoksa koşullu olarak birkaç ref’den birini mi ilettiğini bilemezsiniz. Dolayısıyla, Efektiniz hangi ref’in iletildiğine bağlı olacaktır.
3. Adım: Gerekliyse temizleme fonksiyonu ekle
Başka bir örneği ele alalım. Ekranda göründüğünde sohbet sunucusuna bağlanması gereken bir ChatRoom
bileşeni yazıyorsunuz. Elinizde connect()
ve disconnect()
metodlarını içeren bir nesne döndüren createConnection()
API’ı var. Bileşen kullanıcı tarafında görüntülenirken bu bileşeni nasıl bağlı tutarsınız?
Efektin mantığını yazarak başlayın:
useEffect(() => {
const connection = createConnection();
connection.connect();
});
Her yeniden render’dan sonra tekrar sohbete bağlanmak çok yavaş olurdu bu yüzden bağımlılık dizisi ekleyin:
useEffect(() => {
const connection = createConnection();
connection.connect();
}, []);
Efekt içindeki kod herhangi bir prop ya da state kullanmıyor bu yüzden bağımlılık diziniz []
(boş) olacaktır. Bu React’e, bu kodu yalnızca bileşen “DOM’a eklendiğinde” çalıştırmasını söyler. Örneğin, bileşen ekranda ilk defa göründüğünde.
Bu kodu çalıştırmayı deneyelim:
import { useEffect } from 'react'; import { createConnection } from './chat.js'; export default function ChatRoom() { useEffect(() => { const connection = createConnection(); connection.connect(); }, []); return <h1>Sohbete hoşgeldiniz!</h1>; }
Bu Efekt yalnızca bileşen DOM’a eklendiğinde çalışır, bu yüzden "✅ Bağlanıyor..."
yazısının konsola sadece bir kere yazılmasını bekleyebilirsiniz. Ancak, eğer konsolu kontrol ederseniz, "✅ Bağlanıyor..."
iki kere yazılmıştır. Bu niye olmakta?
ChatRoom
bileşeninin birçok farklı ekrana sahip daha büyük bir uygulamanın parçası olduğunu hayal edin. Kullanıcı macerasına ChatRoom
sayfasına başlıyor. Bileşen DOM’a ekleniyor ve connection.connect()
fonksiyonunu çağırıyor. Şimdi ise kullanıcının başka bir sayfaya örneğin Ayarlar sayfasına gittiğini düşünün. ChatRoom
bileşeni DOM’dan kaldırılır. Son olarak, kullanıcı Geri tuşuna basar ve ChatRoom
bileşeni tekrar DOM’a eklenir. Bu ikinci bir bağlantı kurar ancak ilk bağlantı yok edilmemişti! Kullanıcı uygulama sayfalarında gezdikçe bağlantılar birikecektir.
Bu gibi hatalar geniş manuel testler olmadan kolayca gözden kaçırılırlar. Bu hataları hızlıcı farketmeniz için geliştirme sırasında React, bileşen ilk defa DOM’a eklendikten hemen sonra o bileşeni bir kere DOM’dan kaldırır.
"✅ Bağlanıyor..."
mesajını iki kere görmek asıl sorunu farketmenize yardımcı olur: kodunuz, bileşen DOM’dan kaldırıldığında bağlantıyı kesmemektedir.
Bu sorunu düzeltmek için, Efektinizde bir temizleme fonksiyonu döndürün:
useEffect(() => {
const connection = createConnection();
connection.connect();
return () => {
connection.disconnect();
};
}, []);
React, Efektiniz yeniden çalışmadan önce her seferinde temizleme fonksiyonunuzu çağıracak ve son olarak bileşen DOM’dan kaldırıldığında çağıracaktır. Temizleme fonksiyonu yazıldığı zaman ne olduğunu görelim:
import { useState, useEffect } from 'react'; import { createConnection } from './chat.js'; export default function ChatRoom() { useEffect(() => { const connection = createConnection(); connection.connect(); return () => connection.disconnect(); }, []); return <h1>Sohbete hoşgeldiniz!</h1>; }
Şimdi geliştirme sırasında üç mesaj alırsınız:
"✅ Bağlanıyor..."
"❌ Bağlantı kesildi."
"✅ Bağlanıyor..."
Bu geliştirme sırasındaki doğru davranıştır. React, bileşeninizi DOM’dan kaldırarak, uygulama sayfalarında ileri ve geri gitmenin kodunuzu bozmayacağını doğrular. Bağlantının kesilmesi ve yeniden bağlanılması tam da olması gerekendir! Temizleme fonksiyonunu doğru uyguladığınız zaman, Efekti bir kere çalıştırma ile çalıştırma, temizleme ve tekrar çalıştırma arasında kullanıcı tarafından görülen bir fark olmamalıdır. Burada fazladan bir bağlan/bağlantıyı kes çağrı çifti vardır çünkü React, geliştirme sırasındaki hatalar için kodunuzu araştırmaktadır. Bu normal davranıştır, ondan kurtulmaya çalışmayın!
Son üründe, "✅ Bağlanıyor..."
mesajını bir kere alırsınız. Bileşenlerin DOM’dan kaldırılması yalnızca geliştirme sırasında temizleme gerektiren Efektleri bulmanıza yardımcı olmak için vardır. Strict Modu kapatarak geliştirme sırasındaki davranışlarından kaçınabilirsiniz ancak biz bunu açık tutmanızı tavsiye ediyoruz. Bu, yukarıdaki gibi bir çok hatayı bulmanızı sağlar.
Geliştirme sırasında Efektin iki kere çalışması nasıl kullanılır?
React, son örnekteki gibi hataları bulmak için bileşenlerinizi geliştirme sırasında kasıtlı olarak DOM’dan kaldırır ve ekler. Doğru soru “bir Efekt nasıl bir defa çalıştırılır” değil, “Efektimi bileşen DOM’dan kaldırılıp yeniden eklendikten sonra çalışacak şekilde nasıl düzeltirim” olmalıdır.
Genellikle doğru cevap, temizleme fonksiyonu eklemektir. Temizleme fonksiyonu, Efektin yaptığı her şeyi durdurmalı veya geri almalıdır. Temel kural, kullanıcı bir kez çalışan Efekt (son üründe olduğu gibi) ile kurulum → temizleme → kurulum sekansı (geliştirme sırasında olduğu gibi) arasındaki farkı ayırt etmemelidir.
Yazacağınız Efektlerin çoğu aşağıdaki yaygın kalıplardan birine uyacaktır.
React olmayan widget’ları kontrol etmek
Bazen React’te yazılmamış UI widget’ları eklemek isteyebilirsiniz. Örneğin, sayfanıza bir harita bileşeni ekliyorsunuz. Bu harita setZoomLevel()
metoduna sahip ve React kodunuzdaki zoomLevel
state değişkenini yakınlaştırma seviyesi ile senkronize etmek istiyorsunuz. Efektiniz şuna benzeyecektir:
useEffect(() => {
const map = mapRef.current;
map.setZoomLevel(zoomLevel);
}, [zoomLevel]);
Bu durumda temizleme fonksiyonuna ihtiyacınız yoktur. Geliştirme sırasında React, Efekti iki defa çağıracaktır ancak bu bir problem yaratmaz çünkü setZoomLevel
metodunu aynı değer ile iki defa çağırmak hiçbir şey yapmaz. Bu biraz yavaş olabilir ancak son üründe bileşen gereksiz yere DOM’a eklenip çıkarılmadığından önemli değildir.
Bazı API’lar ard arda iki defa çağırmanıza izin vermezler. Örneğin, tarayıcıya yerleşik <dialog>
elemanının showModal
metodu iki kez çağırılamaz. Temizleme fonksiyonunu yazarak modal dialog’unu kapatmasını sağlayın:
useEffect(() => {
const dialog = dialogRef.current;
dialog.showModal();
return () => dialog.close();
}, []);
Geliştirme sırasında, Efektiniz showModal()
fonksiyonunu çağıracaktır, hemen ardından close()
çağırılacaktır, son olarak yine showModal()
çağırılacaktır. Bu, son üründe görebileceğiniz gibi, showModal()
’ın bir kez çağırılmasıyla kullanıcı tarafından görülen aynı davraşına sahiptir.
Olaylara abone olma
Eğer Efektiniz bir şeye abone oluyorsa (subscribe), temizleme fonksiyonu abonelikten çıkarmalıdır:
useEffect(() => {
function handleScroll(e) {
console.log(window.scrollX, window.scrollY);
}
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
Geliştirme sırasında, Efektiniz addEventListener()
’ı çağıracaktır, hemen ardından removeEventListener()
çağırılacaktır, ve ardından aynı yöneticiyle tekrar addEventListener()
çağırılacaktır. Yani aynı anda yalnızca bir aktif abonelik olacaktır. Bu, son üründe görebileceğiniz gibi, addEventListener()
’ın bir kez çağırılmasıyla kullanıcı tarafından görülen aynı davranışa sahiptir.
Animasyonları tetikleme
Efektiniz bir şeye animasyon ekliyorsa, temizleme fonksiyonu o animasyonu başlangıç değerlerine sıfırlamalıdır:
useEffect(() => {
const node = ref.current;
node.style.opacity = 1; // Animasyonu tetikler
return () => {
node.style.opacity = 0; // Başlangıç değerlerine sıfırlar
};
}, []);
Geliştirme sırasında, opaklık (opacity) 1
olacaktır, daha sonra 0
, ardından tekrar 1
olacaktır. Bu, son üründe görebileceğiniz gibi, opaklık değerinin 1
’e eşitlenmesi kullanıcı tarafından görülen aynı davranışa sahiptir. Eğer ara doldurma (tweening) destekli üçüncü parti bir animasyon kütüphanesini kullanıyorsanız, temizleme fonksiyonunuz zaman çizelgesini başlangıç state’ine sıfırlamalıdır.
Veri getirme
Efektiniz bir veri getiriyorsa, temizleme fonksiyonunuz ya veri getirmeye iptal etmeli ya da getirilen sonucu yok saymalıdır:
useEffect(() => {
let ignore = false;
async function startFetching() {
const json = await fetchTodos(userId);
if (!ignore) {
setTodos(json);
}
}
startFetching();
return () => {
ignore = true;
};
}, [userId]);
Çoktan gerçekleştirilmiş bir ağ isteği “geri alınamaz” ama temizleme işleviniz artık alakalı olmayan bir veri getirme işleminin uygulamanızı etkilemesini engellemelidir. userId
, 'Alice'
’ten 'Bob'
’a değişirse, temizleme fonksiyonu, 'Alice'
yanıtı 'Bob'
’tan önce gelse bile 'Alice'
yanıtının göz ardı edilmesini sağlar.
Geliştirme sırasında, Network sekmesinde (developer tools) iki veri getirme işlemi göreceksiniz. Bunda hiçbir sorun yok. Yukarıdaki yaklaşımla, ilk Efekt hemen temizlenecek ve böylece ignore
değişkeninin kopyası true
olacaktır. Yani fazladan bir istek olsa bile, if (!ignore)
ifadesi sayesinde state’i etkilemeyecektir.
Son üründe, sadece tek bir istek olacaktır. Geliştirme sırasındaki ikinci istek sizi rahatsız ediyorsa, en iyi yaklaşım, istekleri tekilleştiren ve sonuçlarını bileşenler arasında önbelleğe alan bir çözüm kullanmaktır:
function TodoList() {
const todos = useSomeDataLibrary(`/api/user/${userId}/todos`);
// ...
Bu sadece geliştirme deneyimini iyileştirmekle kalmaz aynı zamanda uygulamanız daha hızlı çalıştığını hissettirir. Örneğin, Geri butonuna basan bir kullanıcının, önbelleğe alınacağı için bazı verileri beklemesine gerek kalmayacaktır. Böyle bir önbelleği kendiniz yapabilir ya da Efektte manuel olarak veri getirmeye yardımcı olan pek çok alternatiften birini kullanabilirsiniz.
Derinlemesine İnceleme
Efektler içinde veri getirme
çağrıları yazmak özellikle tamamen kullanıcı taraflı uygulamalarda veri getirmenin popüler bir yoludur. Ancak bu, oldukça manuel bir yaklaşımdır ve önemli dezavantajları vardır:
- Efektler sunucuda çalışmazlar. Bu, sunucu tarafından render edilen ilk HTML’in veri içermeyen bir yükleme state’ini içereceği anlamına gelir. Kullanıcı bilgisayarının tüm bu JavaScript’i indirmesi ve uygulamanızın verileri yüklemesi gerektiğini keşfetmesi için render etmesi gerekecektir. Bu çok verimli bir yol değildir.
- Doğrudan Efekt ile veri getirmek, “ağ şelaleleri (waterfalls) oluşturmayı kolaylaştırır.” Üst bileşeni render edersiniz, o bileşen veri getirir, alt bileşenleri render eder, daha sonra o bileşenler kendi verilerini getirmeye başlarlar. Eğer internet bağlantınız hızlı değilse, verileri paralel olarak getirmeye göre önemli derecede yavaştır.
- Doğrudan Efekt ile veri getirmek, genellikle verileri önceden yüklememeniz veya önbelleğe almamanız anlamına gelir. Örneğin, bileşen DOM’dan kaldırılır ve sonra tekrar DOM’a eklenirse, bileşen aynı veriyi tekrar getirmek zorundadır.
- Ergonomik değildir. Yarış koşulları gibi hatalardan zarar görmeyecek şekilde
fetch
çağrıları yaparken oldukça fazla genel hatlarıyla (boilerplate) kod yazmanız gerekmektedir.
Bu dezavantajlar listesi React’e özel değildir. Bu, herhangi bir kütüphane ile DOM’a eklenme sırasında yapılan veri getirme için geçerlidir. Yönlendirme (routing) de olduğu gibi, veri getirmenin de başarılı şekilde yapılması kolay değildir. Bu nedenle aşağıdaki yaklaşımları önermekteyiz:
- Eğer bir çatı kullanırsanız, çatının yerleşik veri getirme mekanizmasını kullanın. Modern React çatıları verimli veri getirme mekanizmalarını entegre etmişlerdir ve yukarıdaki tehlikelerden uzak dururlar.
- Aksi halde, kullanıcı taraflı önbelleğe almayı kullanmayı ya da kendiniz kurmayı düşünün. Popüler açık kaynak çözümleri arasında React Query, useSWR ve React Router 6.4+ vardır. Kendi çözümlerinizi de oluşturabilirsiniz. Kendi çözümünüzü yaparsanız, Efektleri arka planda kullanır ancak aynı zamanda istekleri tekilleştirmek, yanıtları önbelleğe almak ve ağ şelalelerinden kaçınmak (verileri önceden yükleyerek veya veri gereksinimlerini rotalara kaldırarak) gibi mantıkları da ekleyebilirsiniz.
Eğer bu yaklaşımlardan hiçbiri size uymuyorsa, Efektler içinde veri getirmeye devam edebilirsiniz.
Analizleri gönderme
Sayfa ziyaretinde analiz gönderen bu kodu düşünün:
useEffect(() => {
logVisit(url); // POST isteği gönderir
}, [url]);
Geliştirme sırasında, logVisit
fonksiyonu her URL için iki defa çağrılacaktır, bu yüzden bunu düzeltmeye çalışabilirsiniz. Bu kodu olduğu gibi bırakmanızı öneririz. Önceki örneklerde olduğu gibi, bir kez çalıştırmak ile iki kez çalıştırmak arasında kullanıcının görebileceği bir davranış farkı yoktur. Pratik açıdan, logVisit
geliştirme sırasında hiçbir şey yapmamalılıdır çünkü geliştirme makinelerinden gelen bilgilerin son üründeki ölçümleri çarpıtmasını istemezsiniz. Bileşeniniz dosyasını her kaydettiğiniz sefer DOM’dan çıkarılıp/eklenir, bu yüzden zaten geliştirme sırasındaki ekstra ziyaretleri kaydeder.
Son üründe, kopyalanmış ziyaret kayıtları olmayacaktır.
Gönderdiğiniz analiz olaylarını debug etmek (hatayı düzeltmek) için, uygulamanızı bir hazırlama ortamına (son ürün modunda çalışan) veya Strict Mode ve yalnızca geliştirme sırasındaki DOM’dan çıkarıp/ekleme kontrollerini geçici olarak devre dışı bırakabilirsiniz. Efektler yerine yol (route) değişikliği olay yöneticilerinden de analizleri gönderebilirsiniz. Daha kesin analizler için, kesişme gözlemcileri (intersection observers), görünüm alanında hangi bileşenlerin olduğunu ve bu bileşenlerin ne kadar süre görünür kaldıklarını izlemeye yardımcı olabilir.
Efekt değil: Uygulamanın başlatılması
Yazdığınız bazı mantıklar uygulama başlatıldığında yalnızca bir kez çalışmalıdır. Bu mantıkları bileşenlerinizin dışına koyabilirsiniz:
if (typeof window !== 'undefined') { // Tarayıcıda çalışıp çalışmadığımızı kontrol et
checkAuthToken();
loadDataFromLocalStorage();
}
function App() {
// ...
}
Bu, böyle bir mantığın, tarayıcı sayfayı yükledikten sonra yalnızca bir defa çalışacağını garanti eder.
Efekt değil: Ürün satın alma
Bazen, temizleme fonksiyonu yazsanız bile, Efektin iki kez çalıştırılmasından dolayı kullanıcının görebileceği sonuçları önlemenin bir yolu yoktur. Örneğin, belki Efektiniz ürün satın almak için bir POST isteği gönderiyor:
useEffect(() => {
// 🔴 Yanlış: Bu Efekt geliştirme sırasında iki kez çalışır, koddaki bir problemi ortaya çıkarır.
fetch('/api/buy', { method: 'POST' });
}, []);
Ürünü iki defa almak istemezsiniz. Ancak, bu yüzden de bu mantığı Efektin içine koymamalısınız. Ya kullanıcı başka bir sayfaya giderse ve Geri butonuna basarsa? Efektiniz tekrardan çalışacaktır. Ürünü kullanıcı sayfayı ziyaret ettiğinde değil, kullanıcı Satın Al butonuna bastığında almak istersiniz.
Satın almak render etmekten değil, spesifik bir etkileşimden kaynaklanmaktadır. Yalnızca kullanıcı butona bastığında çalışmalıdır. Efekti silin ve /api/buy
isteğini Satın Al butonu olay yöneticisine taşıyın:
function handleClick() {
// ✅ Satın almak bir olaydır çünkü spesifik bir etkileşimden kaynaklanmaktadır.
fetch('/api/buy', { method: 'POST' });
}
Bu, eğer DOM’dan çıkarıp/eklemek uygulamanızın mantığını bozarsa, bunun genellikle mevcut hataları ortaya çıkaracağını gösterir. Kullanıcı açısından, bir sayfayı ziyaret etmek, o sayfayı ziyaret etmekten, bir bağlantıya tıklayıp Geri butonuna basmaktan farklı olmamalıdır. React, geliştirme sırasında bileşenleri bir kez DOM’dan çıkarıp/ekleyerek bileşenlerinizin bu prensibe uyduğunu doğrular.
Hepsini bir araya koyma
Bu örnek, Efektlerin pratikte nasıl çalıştığına dair “bir fikir edinmenize” yardımcı olabilir.
Bu örnek, input metinini içeren bir konsol mesajını Efekt çalıştıktan üç saniye sonra görünecek şekilde planlamak için setTimeout
API’ını kullanır. Temizleme fonksiyonu bekleyen zaman aşımını iptal eder. “Bileşeni DOM’a ekle” butonuna basarak başlayın:
import { useState, useEffect } from 'react'; function Playground() { const [text, setText] = useState('a'); useEffect(() => { function onTimeout() { console.log('⏰ ' + text); } console.log('🔵 "' + text + '" metnini planla'); const timeoutId = setTimeout(onTimeout, 3000); return () => { console.log('🟡 "' + text + '" metnini iptal et'); clearTimeout(timeoutId); }; }, [text]); return ( <> <label> Ne yazılmalı:{' '} <input value={text} onChange={e => setText(e.target.value)} /> </label> <h1>{text}</h1> </> ); } export default function App() { const [show, setShow] = useState(false); return ( <> <button onClick={() => setShow(!show)}> Bileşeni {show ? "DOM'dan çıkar" : "DOM'a ekle"} </button> {show && <hr />} {show && <Playground />} </> ); }
İlk başta üç mesaj göreceksiniz: "a" mesajını planla
, "a" mesajını iptal et
ve tekrardan "a" mesajını planla
. Üç saniye sonra a
yazan başka bir mesaj olacaktır. Daha önce öğrendiğiniz gibi, fazladan planla/iptal et işlemi, React’in geliştirme sırasında, temizleme fonksiyonunu doğru yazdığınızı kontrol etmek amacıyla bileşeni DOM’dan çıkarıp/eklemesidir.
Input’u abc
olarak düzenleyin. Eğer bunu yeterince hızlı yaparsanız, "ab" mesajını planla
’dan hemen sonra "ab" mesajını iptal et
ve "abc" mesajını planla
mesajlarını göreceksiniz. React her zaman bir sonraki render’ın Efektinden önce bir önceki render’ın Efektini temizler. Bu yüzden input’a hızlı yazsanız bile, aynı anda en fazla bir zaman aşımı planlanmıştır. Efektlerin nasıl temizlendiğini anlayabilmek için input’u düzenleyin ve konsolu inceleyin.
Input’a bir şeyler yazın ve hemen ardından “Bileşeni DOM’dan kaldır” butonuna basın. Bileşeni DOM’dan kaldırmanın nasıl son render’ın Efektini temizlediğine dikkat edin. Burada, tetikleme şansı bulamadan son zaman aşımını temizler.
Son olarak, zaman aşımlarının iptal edilmemesi için yukırdaki bileşeni düzenleyin ve temizleme fonksiyonunu devre dışı bırakın. Hızlı şekilde abcde
yazmaya çalışın. Üç saniye içinde ne olmasını bekliyorsunuz? Zaman aşımı içindeki console.log(text)
ifadesi son text
’i ve beş tane abcde
mesajı mı yazdıracaktır? Bir deneyip ne olacağını görün!
Üç saniye sonra, beş abcde
mesajı yerine bir seri (a
, ab
, abc
, abcd
, ve abcde
) mesajı göreceksiniz. Her Efekt, karşılık gelen render’ın text
değerini yakalar. text
state’inin değişmesi önemli değildir: text = 'ab'
ile oluşan render’ın Efekti her zaman 'ab'
ifadesini görecektir. Diğer bir deyişle, her bir render’ın Efekti birbirinden izole bir şekildedir. Eğer bunun nasıl çalıştığını merak ediyorsanız, closure’ler hakkında bilgi alabilirsiniz.
Derinlemesine İnceleme
useEffect
’i render çıktısına bir davranış parçasının “iliştirilmesi” olarak düşünebilirsiniz. Bu Efekti düşünün:
export default function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
return <h1>{roomId} odasına hoşgeldiniz!</h1>;
}
Kullanıcı uygulama içinde gezindiğinde tam olarak neler olduğunu görelim.
İlk render
Kullanıcı <ChatRoom roomId="genel" />
ziyaret eder. Şimdi mental olarak roomId
’yi 'genel'
olarak değiştirelim:
// İlk render için JSX (roomId = "general")
return <h1>Genel odasına hoşgeldiniz!</h1>;
Efekt aynı zamanda render çıktısının bir parçasıdır. İlk render’ın Efekti şöyle olur:
// İlk render için Efekt (roomId = "genel")
() => {
const connection = createConnection('genel');
connection.connect();
return () => connection.disconnect();
},
// İlk render için bağımlılıklar (roomId = "genel")
['genel']
React 'genel'
sohbet odasına bağlanan Efekti çalıştırır.
Aynı bağımlılıklar ile yeniden render etme
Diyelim ki <ChatRoom roomId="genel" />
yeniden render edilmektedir. JSX çıktısı aynıdır:
// İkinci render için JSX (roomId = "genel")
return <h1>Genel odasına hoşgeldiniz!</h1>;
React render edilen çıktının değişmediğini görecektir bu yüzden DOM’u güncellemez.
İkinci render’ın Efekti şöyle gözükmektedir:
// İkinci render için Efekt (roomId = "genel")
() => {
const connection = createConnection('genel');
connection.connect();
return () => connection.disconnect();
},
// İkinci render için bağımlılıklar (roomId = "genel")
['genel']
React, ilk render’dan ['genel']
ile ikinci render’dan ['genel']
’i kıyaslar. Tüm bağımlılıkların aynı olmasından dolayı, React ikinci render’daki Efekti görmezden gelecektir. İkinci olan asla çağırılmaz.
Farklı bağımlılıklar ile yeniden render etme
Daha sonra, kullanıcı <ChatRoom roomId="seyahat" />
ziyaret eder. Bu sefer, bileşen başka bir JSX döndürür:
// Üçüncü render için JSX (roomId = "seyahat")
return <h1>Seyahat odasına hoşgeldiniz!</h1>;
React, "Genel odasına hoşgeldiniz"
yazısını "Seyahat odasına hoşgeldiniz"
olarak değiştirmek için DOM’u günceller.
Üçüncü render’ın Efekti şu şekilde gözükmektedir:
// Üçüncü render için Effect (roomId = "seyahat")
() => {
const connection = createConnection('seyahat');
connection.connect();
return () => connection.disconnect();
},
// Üçüncü render için bağımlılıklar (roomId = "seyahat")
['seyahat']
React, ikinci render’dan ['genel']
ile üçüncü render’dan ['seyahat']
’i kıyaslar. Bir bağımlılık farklı: Object.is('seyahat', 'genel')
false
olur. Efekt devreye girer.
React üçüncü render’daki Efekti uygulamadan önce, en son çalışan Efekti temizlemelidir. İkinci render’ın Efekti atlandığından dolayı React’in ilk render’ın Efektini temizlemesi gerekmektedir. Eğer ilk render’a bakacak olursanız, ilk render’ın temizleme fonksiyonunun createConnection('genel')
ile yapılmış olan bağlantıda disconnect()
fonksiyonunu çağırdığını göreceksiniz. Böylelikle uygulama 'genel'
sohbet odasıyla bağlantıyı koparacaktır.
Bundan sonra, React üçüncü render’ın Efektini çalıştırır. Uygulama 'seyahat'
sohbet odasına bağlanır.
DOM’dan kaldırma
Son olarak, diyelim ki kullanıcı başka sayfaları ziyaret eder ve ChatRoom
bileşeni DOM’dan kaldırılır. React son Efektin temizleme fonksiyonunu çalıştırır. Son Efekt üçüncü render’ın Efektiydi. Üçüncü render’ın temizleme fonksiyonu createConnection('seyahat')
bağlantısını koparır. Yani uygulama 'seyahat'
odasıyla bağlantısını koparır.
Sadece geliştirme sırasında olan davranışlar
Strict modu açıkken, React her bir bileşeni DOM’a ekledikten sonra DOM’dan kaldırır (state ve DOM korunur). Bu, temizleme fonksiyonu gereken Efektleri bulmanıza yardımcı olur ve yarış koşulları gibi hataları erken ortaya çıkarır. Ek olarak, geliştirme sırasında dosyayı her kaydettiğiniz zaman React, Efektleri DOM’dan çıkarıp/ekleyecektir. Bu her iki davranışta sadece geliştirme sırasında olmaktadır.
Özet
- Olayların aksine, Efektler belirli bir etkileşim sonucu değil de render etmenin kendisinden kaynaklanmaktadır.
- Efektler bir bileşeni bir harici sistem ile senkronize etmenizi sağlar (üçüncü parti API, ağ vb).
- Varsayılan olarak, Efektler her render’dan sonra çalışır (ilk render’da dahil).
- Eğer Efektin bağımlılıklarının değerleri son yapılan render’da değişmemişse React Efekti çalıştırmayacaktır.
- Bağımlılıklarınızı “seçemezsiniz”. Bağımlılıklar Efekt içindeki koda bağlı olarak belirlenir.
- Boş bağımlılık dizisi (
[]
) bileşen “DOM’a ekleniyor” demektir, örneğin bileşenin ekrana eklenmesi. - Strict modundayken, React bileşenleri DOM’a Efektlerinize stres testi yapmak için iki defa ekler (sadece geliştirme sırasında!).
- Efektiniz DOM’dan çıkarılıp/eklenme aşamasında uygulamanızı bozuyorsa, bir temizleme fonksiyonu yazmanız gerekmektedir.
- React temizleme fonksiyonunuzu Efektin bir sonraki çalışmasından önce ve DOM’dan çıkarılma aşamasına çağıracaktır.
Problem 1 / 4: Bileşen DOM’a eklendiğinde alana odaklanma
Bu örnekte, form, <MyInput />
bileşenini render etmektedir.
Input’un focus()
metodunu kullanarak MyInput
bileşeni ekranda göründüğünde input alanına otomatik olarak odaklanmasını sağlayın. Halihazırda yorum satırı içine alınmış bir örnek var ancak tam olarak çalışmamakta. Niye çalışmadığını bulun ve düzeltin. (Eğer autoFocus
’u biliyorsanız, yokmuş gibi davranın: aynı işlevselliği sıfırdan yeniden yazıyoruz.)
import { useEffect, useRef } from 'react'; export default function MyInput({ value, onChange }) { const ref = useRef(null); // YAPILACAK: Bu tam olarak çalışmamakta. Düzeltin. // ref.current.focus() return ( <input ref={ref} value={value} onChange={onChange} /> ); }
Çözümünüzün çalıştığını doğrulamak için “Formu göster” butonuna tıklayın ve input’a odaklandığını kontrol edin (input alanı farklı renk alır ve içine imleç yerleştirilir). “Formu gizle” ve “Formu göster” butonuna tıklayın. Input alanına yeniden odaklandığını kontrol edin.
MyInput
her render’dan sonra değil sadece DOM’a eklendiğinde odaklanmalıdır. Bu davranışın çalıştığını doğrulamak için “Formu göster” butonuna tıklayın ve ardından “Büyük harf yap” kutucuğuna birkaç defa tıklayın. Kutucuğa tıklamak yukarıdaki input’a odaklamamalıdır.