Bir Efekte İhtiyacınız Olmayabilir

Efektler, React paradigmasından bir kaçış yoludur. Bu kaçış yolları size React’ten “dışarı çıkmanıza” ve React ile alakalı olmayan React araçlarıyla, ağ veya tarayıcı DOM’u gibi bazı harici sistemlerle bileşenlerinizi senkronize etmenize izin verir. Eğer harici bir sistem yoksa (örneğin, bir bileşenin state’ini bazı props veya state değişikliklerinde güncellemek istiyorsanız), bir Efekte ihtiyacınız olmamalıdır. Gereksiz Efektleri ortadan kaldırmak kodunuzun takip edilmesini kolaylaştıracak, çalışmasını hızlandıracak ve hataya daha az açık hale getirecektir.

Bunları öğreneceksiniz

  • Gereksiz Efektleri bileşenlerinizden neden ve nasıl ortadan kaldırabileceğinizi
  • Masraflı hesaplamaları Efektler olmadan nasıl önbelleğe alabileceğinizi
  • Efektler olmadan bileşen state’ini nasıl ayarlayıp ve sıfırlayabileceğinizi
  • Olay yöneticileri arasında mantığı nasıl paylaşabileceğinizi
  • Ne tür mantık kodlarının olay yöneticilerine taşınabileceğini
  • Üst elemanlara değişiklikler hakkında nasıl bildirimde bulunulacağını

Gereksiz Efektler nasıl ortadan kaldırılır

Efektlere ihtiyaç duymadığınız iki yaygın durum vardır:

  • Verileri işlemek üzere dönüştürmek için Efektlere ihtiyacınız yoktur. Örneğin, bir listeyi göstermeden önce o listeyi filtrelemek istediğinizi varsayalım. Liste değiştiğinde bir state değişkenini güncelleyen bir Efekt yazmak cazip hissettirebilir. Ancak, bu yöntem verimsizdir. State’i güncellediğinizde, React ilk olarak ekranda ne gözükeceğini hesaplamak için öncelikle bileşen fonksiyonlarınızı çağırır. Daha sonra React ekranı güncelleyerek bu değişiklikleri DOM’a “işleyecektir”. Ardından React Efektlerinizi çalıştıracaktır. Efektiniz ayrıca state’i anında güncelliyorsa, bu tüm süreci yeniden sıfırdan başlatır! Gereksiz render geçişlerini önlemek için bileşenlerinizin en üst düzeyindeki tüm verileri dönüştürün. Bu kod propslarınız veya stateleriniz değiştiğinde otomatik olarak yeniden çalışacaktır.
  • Kulanıcı olaylarını yönetmek için Efektlere ihtiyacınız yoktur. Örneğin, /api/buy POST isteği göndermek ve kullanıcı bir ürün satın aldığında bir bildirim göstermek istediğinizi varsayalım. Satın Al buton olay yöneticisi içerisinde, kesinlikle ne olacağını bilirsiniz. Efekt çalıştığında, kullanınıcının ne yaptığını bilemezsiniz (örneğin, hangi butona tıklandığını). Bu sebeple, genellikle kullanıcı olaylarını karşılık gelen olay yöneticileri içerisinde ele alacaksınız.

Dış sistemlerle senkronize olmak için Efektleri kullanmanız gerekmektedir. Örneğin, bir jQuery bileşenini React state’i ile senkronize eden bir Efekt yazabilirsiniz. Ayrıca Efektler ile data çekebilirsiniz: Örneğin, mevcut arama sorgusuyla arama sonuçlarını senkronize edebilirsiniz. Unutmayın ki modern frameworkler bileşenlerinizde doğrudan Efektler yazmak yerine daha verimli ve entegre veri çekme mekanizmaları sunarlar.

Doğru sezgiyi kazanmanıza yardımcı olmak için, hadi bazı yaygın somut örneklere göz atalım!

State veya propslara göre state’i güncelleme

İki state değişkenine sahip bir bileşeniniz olduğunu varsayalım: firstName ve lastName. firstName ve lastName’i birleştirerek onlardan bir fullName elde etmek istiyorsunuz. Ayrıca, firstName veya lastName her değiştiğinde fullName’i güncellemek istiyorsunuz. İlk olarak aklınıza fullName state değişkeni oluşturmak ve onu bir Efekt içerisinde güncellemek olabilir:

function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');

// 🔴 Gereksiz state ve Efektlerden uzak durun.
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
// ...
}

Bu gerektiğinden daha karmaşıktır. Aynı zamanda verimsizdir: fullName için geçersiz bir değerle tam bir yeniden render işlemi gerçekleştirir ve hemen ardından güncellenmiş değerle tekrar yeniden render eder. State değişkenini ve Efektini kaldırın:

function Form() {
const [firstName, setFirstName] = useState('Taylor');
const [lastName, setLastName] = useState('Swift');
// ✅ Render işlemi sırasında hesaplanması iyidir.
const fullName = firstName + ' ' + lastName;
// ...
}

Mevcut props veya state’ten birşey hesaplanabilirken hesaplanabilen değeri state içerisine koymayın. Bunun yerine, render işlemi sırasında hesaplayın. Bu şekilde kodunuz hızlı (Ekstra “kademeli” güncellemelerden kaçınırsınız), daha basit (bazı kodları ortadan kaldırırsınız), ve daha az hata eğilimlidir (birbiriyle senkronize olmayan farklı state değişkenlerinin neden olduğu hatalardan kaçınırsınız). Bu yaklaşım size yeni geliyorsa, React’ta düşünmek state içerisine nelerin girmesi gerektiğini açıklar.

Maliyetli hesaplamaları önbelleğe almak

Bu bileşen gelen todos propunu filter propsuna göre filtreleme işlemi yaparak visibleTodos değerini hesaplar. Sonuçları state içerisinde depolamak ve bir Efektten güncellemek isteyebilirsiniz:

function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');

// 🔴 Gereksiz state ve Efektlerden uzak durun.
const [visibleTodos, setVisibleTodos] = useState([]);
useEffect(() => {
setVisibleTodos(getFilteredTodos(todos, filter));
}, [todos, filter]);

// ...
}

Önceki örnekte olduğu gibi, bu gereksiz ve verimsizdir. İlk olarak, state ve Efekti kaldırın:

function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// ✅ getFilteredTodos() yavaş değilse bu problem değildir.
const visibleTodos = getFilteredTodos(todos, filter);
// ...
}

Genellikle, bu kod iyidir! Ama belki getFilteredTodos() fonksiyonu yavaştır veya bir sürü todos’a sahipsindir. Bu durumda, newTodo gibi alakasız bir state değişkeni değiştiyse, getFilteredTodos()’un yeniden hesaplama yapmasını istemezsin

Maliyetli bir hesaplamayı useMemo Hook’una sarmalayarak önbelleğe alabilirsiniz (veya “memoize edebilirsiniz”):

import { useMemo, useState } from 'react';

function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
const visibleTodos = useMemo(() => {
// ✅ todos veya filter değişmeden yeniden çalışmaz.
return getFilteredTodos(todos, filter);
}, [todos, filter]);
// ...
}

Veya, tek bir satır olarak yazılır:

import { useMemo, useState } from 'react';

function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// ✅ todos veya filtre değiştirilmedikçe getFilteredTodos()'u yeniden çalışmaz.
const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
// ...
}

Bu React’a todos veya filter değişmedikçe iç fonksiyonun yeniden çalışmasını istemediğinizi söyler. ** React getFilteredTodos()’un başlangıç render işlemindeki dönüş değerini hatırlayacaktır. React sonraki render işlemlerinde ise, todos veya filter’ın değişip değişmediğini kontrol edecektir. Eğer bunlar son seferdekiyle aynı ise, useMemo depoladığı son sonucu döndürecektir. Ancak eğer bunlar farklı ise, React iç fonksiyonu tekrar çağıracaktır (ve sonucunu depolayacaktır).

useMemo içerisine sarmaladığınız fonksiyon render işlemi sırasında çalışır, dolayısıyla bu sadece saf hesaplamalar için çalışır.

Derinlemesine İnceleme

Bir hesaplamanın maliyetli olup olmadığı nasıl anlaşılır?

Genel olarak, binlerce nesne oluşturmadıkça veya üzerinde döngü yapmadıkça, bu muhtemelen maliyetli değildir. Daha fazla güven sağlamak isterseniz, bir kod parçasında geçen süreyi ölçmek için bir konsol ekleyebilirsiniz.

console.time('filter array');
const visibleTodos = getFilteredTodos(todos, filter);
console.timeEnd('filter array');

Ölçüm yaptığınız etkileşimi gerçekleştirin (örneğin, giriş kutusuna yazma işlemi yapın). Daha sonra konsolunuzda filter array: 0.15ms gibi loglar göreceksiniz. Toplamda kaydedilen süre miktarı (örneğin, 1ms veya daha fazlası) geçiyorsa, o hesaplamanın önbelleğe alınması mantıklı olabilir. Denemek için, hesaplamayı useMemo ile sarmalayabilir ve bu etkileşim için loglanan toplam sürenin azaldığını doğrulayabilirsiniz:

console.time('filter array');
const visibleTodos = useMemo(() => {
return getFilteredTodos(todos, filter); // todos ve filter değişmediyse atlanır.
}, [todos, filter]);
console.timeEnd('filter array');

useMemo ilk render işlemini daha hızlı yapmaz. Sadece güncellemelerle ilgili gereksiz çalışmaları atlamanıza yardımcı olur.

Makinenizin kullanıcılarınızdan daha hızlı olduğunu aklınızda bulundurun bu nedenle performansınızı yapay bir yavaşlık ile test etmek daha iyi bir fikirdir. Örneğin, Chrome bunun için CPU Throttling seçeneği sunuyor.

Ayrıca geliştirme ortamı içerisinde performans ölçümü yapılması size en doğru sonuçları vermeyeceğini unutmayın. (Örneğin, Strict mod açıkken, her bileşenin bir yerine iki kez render olduğunu göreceksiniz.) En doğru ölçümleri elde etmek için, uygulamanızı üretim için derleyin ve kullanıcılarınızın sahip olduğu gibi bir cihazda test edin.

Bir prop değiştiğinde tüm state’i sıfırlama

Bu ProfilePage bileşeni bir userId propu alır. Sayfa bir yorum inputu içeriyor ve bu değeri tutması için bir comment state değişkeni kullanıyorsunuz. Bir gün, bir problem olduğunu farkedeceksiniz: bir profilden diğerine geçiş yaptığınızda, comment state’inin sıfırlanmamasıdır. Sonuç olarak, yanlış bir kullanıcının profiline istemediğiniz bir yorum yapmak oldukça kolay olabilir. Bu sorunu çözmek için, userId her değiştiğinde comment state değişkeninin temizlenmesini istersiniz:

export default function ProfilePage({ userId }) {
const [comment, setComment] = useState('');

// 🔴 Bir Efekt içerisinde prop değiştiğinde state'i sıfırlamaktan kaçının.
useEffect(() => {
setComment('');
}, [userId]);
// ...
}

Bu verimlilik açısından etkisizdir çünkü ProfilePage ve içerisindeki alt elemanlar ilk olarak eski değerle birlike render edilecek, ve daha sonra tekrar render edilecektir. Ayrıca bu karmaşıktır çünkü ProfilePage içerisindeki her bileşende bu işlemi yapmanız gerekecektir. Örneğin yorum arayüzü iç içe ise, iç içe yorum state’ini de temizlemek istersiniz.

Bunun yerine, her kullanıcının profiline belirli bir key vererek React’a her kullanıcı profilinin kavramsal olarak farklı bir profil olduğunu bildirebilirsiniz. Bileşeninizi ikiye bölün ve dış bileşenden iç bileşene key özniteliği iletin:

export default function ProfilePage({ userId }) {
return (
<Profile
userId={userId}
key={userId}
/>
);
}

function Profile({ userId }) {
// ✅ Bu ve aşağıdaki herhangi bir state key değişikliğinde otomatik olarak sıfırlanır.
const [comment, setComment] = useState('');
// ...
}

Normalde, React aynı bileşen aynı noktada render edildiğinde state’i korur. Profile bileşenine bir key olarak userId ileterek, React’ten farklı userId’li iki Profile bileşenine herhangi bir state’i paylaşmaması gereken iki farklı bileşen olarak muamele etmesini istiyorsunuz. Key her değiştiğinde (userId olarak ayarladığınız), React DOM’u tekrar oluşturacak ve Profile bileşeninin ve tüm alt öğelerinin state’lerini sıfırlar. Artık profiller arasında gezinirken comment alanı otomatik olarak temizlenecektir.

Bu örnekte, sadece dış ProfilePage bileşeninin dışa aktarıldığını ve projedeki diğer dosyalarda gözüktüğünü unutmayın. ProfilePage’i oluşturan bileşenlerin ProfilePagee key iletmesi gerekmez: Bunun yerine userId’yi normal bir prop olarak iletirler. ProfilePage bileşeninin içindeki Profile bileşenine key olarak iletilmesi, bir uygulama ayrıntısıdır.

Bir prop değişikliğinde bazı state’lerin ayarlanması

Bazen, bir prop değişikliğinde state’in bazı noktalarını sıfırlamak veya ayarlamak isteyebilirsiniz.

Buradaki List bileşeni props olarak items listesini alır, ve seçilen öğeyi selection state değişkeni içerisinde tutar. items propsu farklı bir array aldığında selection state değişkenini sıfırlamak isteyebilirsiniz:

function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);

// 🔴 Bir Efekt içerisinde prop değişikliğinde state ayarlamaktan kaçının.
useEffect(() => {
setSelection(null);
}, [items]);
// ...
}

Bu ideal bir çözüm değildir. items her değiştiğinde, List ve onun alt elemanı ilk başta eski selection değeri ile render olacaktır. Daha sonra React DOM’u güncelleyecek ve Efektleri çalıştıracaktır. Son olarak, setSelection(null) çağrısı List ve onun alt elemanlarının yeniden render işlemine sebebiyet verecektir, ve bu süreci yeniden başlatacaktır.

Öncelikle, Efekti silin. Bunun yerine state’i doğrudan render işlemi sırasında ayarlayın:

function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);

// Render işlemi sırasında state ayarlamak daha iyi bir yöntemdir.
const [prevItems, setPrevItems] = useState(items);
if (items !== prevItems) {
setPrevItems(items);
setSelection(null);
}
// ...
}

Bu şekilde, önceki render işlemindeki bilgiyi depolamak anlamayı zorlaştırabilir, ama bu aynı state’i bir Efekt içerisinde güncellemekten daha iyidir. Yukarıdaki örnekte, setSelection direkt olarak render işlemi sırasında çağrılır. React List bileşenini return ifadesi ile hemen çıkış yaptıktan sonra yeniden render edecektir. React List bileşeninin alt elemanlarını henüz render etmemiştir veya DOM henüz güncellenmemiştir, bu sebeple, List bileşeninin alt elemanları eski selection değeri ile render edilir.

Bir bileşeni render işlemi sırasında güncellediğinizde, React, döndürülen JSX’i yoksayar ve hemen yeniden render işlemini tekrarlar. Çok yavaş kademeli yeniden denemeleri önlemek için, React render işlemi sırasında size sadece aynı bileşenin state’ini güncellemenize izin verir. Eğer, render işlemi sırasında başka bir bileşenin state’ini güncellerseniz, bir hata ile karşılaşırsınız. Döngülerden kaçınmak için items !== prevItems gibi bir koşul ifadesi gereklidir. State’i bı şekilde ayarlayabilirsiniz, ama diğer yan efektler (DOM’u değiştirmek veya zaman aşımlarını ayarlamak gibi) bileşeni saf tutmak için olay yöneticilerinin veya Efektlerin içerisinde kalmalıdır.

Bu kalıp bir Efektten daha verimli olmasına rağmen, çoğu bileşenin buna da ihtiyacı olmamalıdır. Ne şekilde yaparsanız yapın, state’i props’lara veya diğer state’lere göre ayarlamak, veri akışınızı anlamanızı ve hata ayıklama yapmanızı daha zor hale getirecektir. Her zaman tüm state’i bir key ile sıfırlamayı veya herşeyi render işlemi sırasında hesaplamayı yapıp yapamayacağınızı kontrol edin. Örneğin, seçilen itemi depolamak (ve sıfırlamak) yerine, seçili item kimliğini(item ID) saklayabilirsiniz:

function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selectedId, setSelectedId] = useState(null);
// ✅ Herşeyi render işlemi sırasında hesaplamak en iyi yöntemdir.
const selection = items.find(item => item.id === selectedId) ?? null;
// ...
}

Şuan burada state’i “ayarlamanıza” ihtiyacınız yoktur. Seçilmiş ID’li item liste içerisindeyse, seçili olarak kalır. Eğer değilse, selection render işlemi esnasında eşleşen item bulunmadığından dolayı null olarak hesaplanacaktır. Bu davranış farklıdır, ama items seçilen değişiklikleri koruduğu için kısmen daha iyidir.

Olay yöneticileri arasında mantık kodları paylaşmak

İstediğiniz ürünü satın alamınıza izin veren iki butonlu (Satın Al ve Öde) bir ürün sayfanızın olduğunu varsayalım. Kullanıcı ürünü sepete eklediğinde bir bildirim göstermek istiyorsunuz. Her iki butonun showNotification() fonksiyonunu çağırması tekrar eden bir işlem gibi gelebilir, bu yüzden bu mantığı bir Efekte yerleştirmek isteyebilirsiniz:

function ProductPage({ product, addToCart }) {
// 🔴 Bir Efekt içerisinde olaya-özgü bir mantık kodu bulundurmaktan kaçının.
useEffect(() => {
if (product.isInCart) {
showNotification(`Added ${product.name} to the shopping cart!`);
}
}, [product]);

function handleBuyClick() {
addToCart(product);
}

function handleCheckoutClick() {
addToCart(product);
navigateTo('/checkout');
}
// ...
}

Bu Efekt gereksizdir. Muhtemelen bir soruna sebebiyet verecektir. Örneğin, uygulamanızın sayfa yeniden yüklemelerinde alışveris sepetinizi “hatırladığını” varsayalım. Sepetinize ürünü birkez ekleyip ardından sayfayı yeniden yüklerseniz, bildirim tekrar görünecektir. Bu ürünün sayfasını her yenilediğinizde gözükmeye devam edecektir. Bunun sebebi, product.isInCart değeri sayfa yüklenirken zaten true olmasıdır, bu sebeple Efekt tekrar showNotification() fonksiyonunu çağıracaktır.

Bazı kod bloklarının bir Efekt veya olay yöneticisi içerisinde olup olmaması gerektiğinden emin değilseniz, bu kod bloğunun neden çalışması gerektiğini kendinize sorun. Sadece bileşenin kullanıcıya gösterildiği durumlarda çalışması gereken kodlar için Efektleri kullanın. Bu örnekte, bildirim sayfa görüntülendiği için değil, kullanıcı butona bastığı için gözükmelidir! Efekti silin ve paylaşılan mantığı, her iki olay yöneticinden çağrılan bir fonksiyon içine yerleştirin:

function ProductPage({ product, addToCart }) {
// ✅ Olaya özgü mantık kodunun, olay yöneticilerinden çağrılması daha iyi bir seçimdir.
function buyProduct() {
addToCart(product);
showNotification(`Added ${product.name} to the shopping cart!`);
}

function handleBuyClick() {
buyProduct();
}

function handleCheckoutClick() {
buyProduct();
navigateTo('/checkout');
}
// ...
}

Bu hem gereksiz Efektleri ortadan kaldırır hem de hataları düzeltir.

Bir POST isteği göndermek

Bu Form bileşeni iki tür POST isteği gönderir. Bileşen yüklendiğinde bir analitik olay gönderir. Formu doldurup Gönder butonuna tıkladığınızda ise /api/register noktasına bir POST isteği gönderir.

function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');

// ✅ Bileşen görüntülendiği için bu mantık çalışır.
useEffect(() => {
post('/analytics/event', { eventName: 'visit_form' });
}, []);

// 🔴 Bir Efekt içerisinde olaya-özgü bir mantık kodu bulundurmaktan kaçının.
const [jsonToSubmit, setJsonToSubmit] = useState(null);
useEffect(() => {
if (jsonToSubmit !== null) {
post('/api/register', jsonToSubmit);
}
}, [jsonToSubmit]);

function handleSubmit(e) {
e.preventDefault();
setJsonToSubmit({ firstName, lastName });
}
// ...
}

Bir önceki örnekte olduğu gibi aynı kriterleri uygulayalım.

Analitik POST isteği bir Efekt içerisinde kalmalıdır. Çünkü, analitik olayının gönderilme nedeni formun görünür olmasıdır. (Bu geliştirme aşamasında iki kez tetiklenebilir, ancak bu durumla başa çıkmak için buraya bakabilirsiniz.)

Ancak, /api/register POST isteği form görünür olduğu için gönderilmez. Bu isteği yalnızca kullanıcı butona bastığı anda göndermek istersiniz. Bu işlem sadece belirli etkileşimlerde meydana gelmelidir. İkinci Efekti silin ve POST isteğini olay yöneticisine taşıyın:

function Form() {
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');

// ✅ Bileşen görüntülendiği için bu mantık çalışır.
useEffect(() => {
post('/analytics/event', { eventName: 'visit_form' });
}, []);

function handleSubmit(e) {
e.preventDefault();
// ✅ Olaya özgü mantık kodunun, olay yöneticilerinden çağrılması daha iyi bir seçimdir.
post('/api/register', { firstName, lastName });
}
// ...
}

Bir olay yöneticisi veya bir Efekt içine hangi mantığı yerleştireceğinizi seçerken, kullanıcının perspektifinden hangi tür mantık olduğu sorusuna cevap bulmanız gerekmektedir. Eğer bu mantık belirli bir etkileşimden kaynaklanıyorsa, olay yöneticisnde tutun. Eğer kullanıcının bileşeni ekran üzerinde görme eylemiyle ilişkili ise, o zaman Efekt içinde tutun.

Hesaplama zincirleri

Bazen her biri diğer bir state’e dayalı olarak state’in bir parçasını ayarlayan Efektleri zincirlemek isteyebilirsiniz.

function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);
const [isGameOver, setIsGameOver] = useState(false);

// 🔴 State'i yalnızca birbirini tetikleyecek şekilde ayarlayan Efekt Zincirlerinden kaçının.
useEffect(() => {
if (card !== null && card.gold) {
setGoldCardCount(c => c + 1);
}
}, [card]);

useEffect(() => {
if (goldCardCount > 3) {
setRound(r => r + 1)
setGoldCardCount(0);
}
}, [goldCardCount]);

useEffect(() => {
if (round > 5) {
setIsGameOver(true);
}
}, [round]);

useEffect(() => {
alert('Good game!');
}, [isGameOver]);

function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error('Game already ended.');
} else {
setCard(nextCard);
}
}

// ...

Bir problem bu kodun çok verimsiz olmasıdır: bileşenin (ve onun alt elemanlarının) set çağrıları arasında her seferinde yeniden render edilmesidir. Yukarıdaki örnekte, en kötü durumda (setCard → render → setGoldCardCount → render → setRound → render → setIsGameOver → render) alt eleman ağacında üç gereksiz yeniden render işlemi gerçekleşir.

Hatta hızlı olmasa bile, kodunuz geliştikçe yeni gereksinimlere uygun olmayan durumlarla karşılaşabilirsiniz. Örneğin, oyun hareketlerinin geçmişini adım adım izlemek için bir yol eklemek istediğinizi düşünün. Her bir state değişkenini geçmişteki bir değere güncelleyerek bunu yapardınız. Ancak, card state’ini geçmişteki bir değere ayarlamak, Efekt zincirini tekrar tetikler ve gösterilen verileri değiştirir. Bu tür bir kod genellikle sert ve kırılgan olabilir.

Bu durumda, yapabileceğiniz hesaplamaları render işlemi sırasında gerçekleştirmek ve durumu olay yöneticisinde ayarlamak daha iyidir.

function Game() {
const [card, setCard] = useState(null);
const [goldCardCount, setGoldCardCount] = useState(0);
const [round, setRound] = useState(1);

// ✅ Mümkün olduğunca render işlemi sırasında hesaplama yapın.
const isGameOver = round > 5;

function handlePlaceCard(nextCard) {
if (isGameOver) {
throw Error('Game already ended.');
}

// ✅ Sonraki state'i olay yöneticisi içerisinde hesaplayın.
setCard(nextCard);
if (nextCard.gold) {
if (goldCardCount <= 3) {
setGoldCardCount(goldCardCount + 1);
} else {
setGoldCardCount(0);
setRound(round + 1);
if (round === 5) {
alert('Good game!');
}
}
}
}

// ...

Bu yöntem çok daha verimli olacaktır. Ayrıca, oyun geçmişini görüntülemek için bir yol uygularsanız, artık her bir state değişkenini diğer tüm değerleri ayarlayan Efekt zincirini tetiklemeden geçmişten bir hamleye ayarlayabileceksiniz. Birden fazla olay yöneticisi arasında mantığı yeniden kullanmanız gerekiyorsa, bir fonksiyon çıkarabilir ve bu fonksiyonu o olay yöneticilerinden çağırabilirsiniz.

Olay yöneticilerinin içinde, durum bir anlık görüntü gibi davranır. Örneğin, setRound(round + 1) çağrıldıktan sonra bile, round değişkeni kullanıcının butona bastığı anda sahip olduğu değeri yansıtır. Hesaplamalar için bir sonraki değeri kullanmanız gerekiyorsa, const nextRound = round + 1 gibi manuel olarak tanımlama yapmalısınız.

Bazı durumlarda, bir sonraki state’i bir olay yöneticisi içerisinden direkt olarak hesaplayamazsınız. Örneğin, birbirine bağlı çoklu açılır menülerin bulunduğu bir form düşünelim. Bir sonraki açılır menünün seçilen değeri önceki açılır menünün seçilen değerine bağlıdır. Bu durumda, bir Efekt zinciri uygun olabilir çünkü ağ bağlantısı ile senkronizasyon yapmanız gerekmektedir.

Uygulamayı başlatma

Bazı mantık kodları, uygulama yüklendiğinde yalnızca bir kez çalışmalıdır.

Bu işlemi genellikle üst-seviye bileşendeki bir Efekt içine yerleştirmek isteyebilirsiniz.

function App() {
// 🔴 Yalnızca bir kez çalışması gereken mantığa sahip olan Efektlerden kaçının.
useEffect(() => {
loadDataFromLocalStorage();
checkAuthToken();
}, []);
// ...
}

Ancak, bu işlemin canli ortamda iki kere çalıştırıldığını keşfedeceksiniz. Bu durum sorunlara neden olabilir—örneğin, fonksiyonun iki kez çağrılması düşünülmeden tasarlandığı için kimlik doğrulama tokeni geçersiz hale gelebilir. Genel olarak, bileşenleriniz yeniden yerleştirilmeye karşı dayanıklı olmalıdır. Bu, üst-seviye App bileşeniniz için de geçerlidir.

Üretim ortamında pratikte yeniden monte edilmese bile, tüm bileşenlerde aynı kısıtlamalara uymak, kodun taşınmasını ve yeniden kullanılmasını kolaylaştırır. Eğer belirli bir mantığın bileşen başına bir kez değil, uygulama yüklemesi başına bir kez çalışması gerekiyorsa, bu durumu takip etmek için bir üst-seviye değişken ekleyebilirsiniz.

let didInit = false;

function App() {
useEffect(() => {
if (!didInit) {
didInit = true;
// ✅ Uygulama her yüklendiğinde yalnızca bir kez çalışır.
loadDataFromLocalStorage();
checkAuthToken();
}
}, []);
// ...
}

Modül başlatma sırasında ve uygulama render edilmeden önce de çalıştırabilirsiniz:

if (typeof window !== 'undefined') { // Tarayıcıda çalışıp çalışmadığınızı kontrol edin.
// ✅ Uygulama her yüklendiğinde yalnızca bir kez çalışır.
checkAuthToken();
loadDataFromLocalStorage();
}

function App() {
// ...
}

Bileşeninizi içe aktardığınızda, bileşenin sonunda render edilmezse bile üst seviyedeki kod bir kez çalışır. Rastgele bileşenler içe aktarılırken yavaşlama veya beklenmeyen davranışlardan kaçınmak için bu yöntemi aşırı kullanmamaya özen gösterin. Uygulama genelindeki başlatma mantığınızı, App.js gibi kök bileşen modüllerinde veya uygulamanızın giriş noktasında tutun.

Üst elemanları state değişiklikleri hakkında bilgilendirmek

isOn state’i true veya false değerlerini alabilen bir Toggle bileşeni yazdığınızı düşünelim. Geçiş efektini sağlaması için birkaç farklı yol vardır (tıklayarak veya sürükleyerek). Toggle dahili durumu her değiştiğinde üst elemana bildirimde bulunmak istiyorsunuz, böylece bir onChange olayını bir Efektten çağırıyorsunuz:

function Toggle({ onChange }) {
const [isOn, setIsOn] = useState(false);

// 🔴 onChange işleyicisinin çok geç çalıştırılmasından kaçının.
useEffect(() => {
onChange(isOn);
}, [isOn, onChange])

function handleClick() {
setIsOn(!isOn);
}

function handleDragEnd(e) {
if (isCloserToRightEdge(e)) {
setIsOn(true);
} else {
setIsOn(false);
}
}

// ...
}

Daha önce olduğu gibi, bu ideal değil. İlk olarak Toggle kendi state’ini günceller, ve React ekranı günceller. Ardından React, üst elemandan iletilen onChange fonksiyonunu çağıran Effect’i çalıştırır. Şimdi üst eleman, başka bir render geçişi başlatarak kendi state’ini güncelleyecektir. Her şeyi tek geçişte yapmak daha iyi olur.

Efekti silin ve bunun yerine aynı olay yöneticisi içinde her iki bileşenin durumunu güncelleyin:

function Toggle({ onChange }) {
const [isOn, setIsOn] = useState(false);

function updateToggle(nextIsOn) {
// ✅ Tüm güncellemeleri onları tetikleyen olay sırasında gerçekleştirin.
setIsOn(nextIsOn);
onChange(nextIsOn);
}

function handleClick() {
updateToggle(!isOn);
}

function handleDragEnd(e) {
if (isCloserToRightEdge(e)) {
updateToggle(true);
} else {
updateToggle(false);
}
}

// ...
}

Bu yaklaşımla, hem Toggle bileşeni hem de onun üst elemanı, olay sırasında state değişkenlerini günceller. React farklı bileşenlerden güncellemeleri toplu olarak gerçekleştirir, böylece yalnızca bir render geçişi olacaktır.

Ayrıca state’i tamamen kaldırabilir ve bunun yerine üst elemandan isOn değerini alabilirsiniz:

// ✅ Bileşenin, kendi üst elemanı tarafından kontrol edilmesi daha iyidir.
function Toggle({ isOn, onChange }) {
function handleClick() {
onChange(!isOn);
}

function handleDragEnd(e) {
if (isCloserToRightEdge(e)) {
onChange(true);
} else {
onChange(false);
}
}

// ...
}

“State’i yukarı taşımak” üst elemanın kendi state’ini değiştirerek, Toggle’ı tamamen kontrol etmesine olanak tanır. Bu, üst elemanın daha fazla mantık içermesi gerektiği anlamına gelir, ancak genel olarak endişelenmeniz gereken daha az durum olur. Farklı iki state değişkenini senkronize tutmaya çalıştığınızda, bunun yerine state’i yukarı taşımaya çalışın!

Üst elemana veri aktarma

Bu Child bileşeni bazı verileri çeker ve ardından Parent bileşenine bir Efekt içerisinde bu veriyi aktarır:

function Parent() {
const [data, setData] = useState(null);
// ...
return <Child onFetched={setData} />;
}

function Child({ onFetched }) {
const data = useSomeAPI();
// 🔴 Verileri bir Efekt içinde üst elemana iletmekten kaçının.
useEffect(() => {
if (data) {
onFetched(data);
}
}, [onFetched, data]);
// ...
}

React içerisinde, veri akışı üst elemanlardan alt elemanlara doğru akar. Ekranda yanlış bir şey gördüğünüzde, yanlış bilgiyi nereden aldığınızı bulmak için bileşen hiyerarşisini yukarı doğru takip edebilirsiniz. Yanlış prop ileten veya yanlış state’e sahip olan bileşeni bulana kadar bileşen zincirinde yukarı doğru ilerleyebilirsiniz. Bu şekilde, sorunun kaynağını tespit edebilir ve düzeltmeler yapabilirsiniz. Alt elemanlar, üst elemanlarının state’ini Efektler içerisinde güncellediği durumlarda, veri akışını takip etmek zorlaşabilir. Üst ve alt elemanın aynı veriye ihtiyacı olduğunda, üst elemanın ihtiyaç duyduğunuz veriyi çekmesini sağlayın ve alt elemanlarına doğru veriyi aşağıya iletin:

function Parent() {
const data = useSomeAPI();
// ...
// ✅ Veriyi aşağı doğru alt elemanlara iletmek daha iyidir.
return <Child data={data} />;
}

function Child({ data }) {
// ...
}

Veri akışının üst elemandan alt elemana doğru olması veri akışının tahmin edilmesini basitleştirir ve daha anlaşılır olmasını sağlar.

Harici veri depolarını takip etme

Bazen, bileşenlerinizin React state’inin dışındaki bazı verilere abone olması gerebilir. Bu veriler, 3.parti bir kütüphaneden veya yerleşik tarayıcı API’leri olabilir. Bu veriler, React’ın bilgisi olmadan değişebileceğinden, manuel olarak bu verileri takip etmeniz gerekmektedir. Bu genellikle bir Efekt ile yapılır, örneğin:

function useOnlineStatus() {
// Bir Efekt içinde manuel veri deposu takip edilmesi ideal değildir.
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function updateState() {
setIsOnline(navigator.onLine);
}

updateState();

window.addEventListener('online', updateState);
window.addEventListener('offline', updateState);
return () => {
window.removeEventListener('online', updateState);
window.removeEventListener('offline', updateState);
};
}, []);
return isOnline;
}

function ChatIndicator() {
const isOnline = useOnlineStatus();
// ...
}

Bu örnekte, bileşen harici bir veri deposunu (burada, tarayıcının navigator.onLine API’sini) takip eder. Bu API sunucuda mevcut olmamasından dolayı (bu sebeple, başlangıç HTML’i için kullanılamaz), başlangıçta state true olarak ayarlanacaktır. Tarayıcı içerisindeki veri deposunun değeri her değiştiğinde, bileşen state’ini günceller.

Bu olay için Efektler kullanmak yaygın olsa da, React’in tercih edilen şekilde kullanılan harici bir veri deposunu takip etmek için özel olarak tasarlanmış bir Hook’u bulunmaktadır. Efekti silin ve useSyncExternalStore ile değiştirin:

function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}

function useOnlineStatus() {
// ✅ Harici veri depolarını yerleşik Hooklar ile takip etmek daha iyidir.
return useSyncExternalStore(
subscribe, // React, aynı fonksiyonu geçtiğin sürece yeniden takip etmeyecek
() => navigator.onLine, // İstemcideki değer bu şekilde alınır
() => true // Sunucudaki değer bu şekilde alınır
);
}

function ChatIndicator() {
const isOnline = useOnlineStatus();
// ...
}

Bu yaklaşım, bir Efekt ile değiştirilebilir React state’ini manuel olarak senkronize etme işlemine göre daha az hataya sebep olur. Genellikle, yukarıdaki useOnlineStatus() gibi özelleştrilmiş bir Hook yazacağınızdan dolayı, ayrı ayrı her bileşende bu işlemi tekrar etmenize gerek yoktur. React bileşenlerinden harici veri depolarını takip etme hakkında daha fazla bilgi edinebilirsiniz.

Veri çekme

Birçok uygulama veri çekme işlemi için Efektleri kullanır. Şu şekilde veri çekme Efekti yazmak oldukça yaygındır:

function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [page, setPage] = useState(1);

useEffect(() => {
// 🔴 Temizleme mantığı olmadan veri çekmekten kaçının.
fetchResults(query, page).then(json => {
setResults(json);
});
}, [query, page]);

function handleNextPageClick() {
setPage(page + 1);
}
// ...
}

Bu veri çekme işlemini bir olay yöneticisine taşımanıza gerek yoktur.

Bu, daha önceki örneklerle çelişkili gibi görünebilir, çünkü mantığı olay yöneticilerine koymak gerekiyordu! Bununla birlikte, düşünün ki veri çekmenin ana neden yazma olayı değildir. Arama inputları genellikle URL’den önceden doldurulur ve kullanıcı, inputa dokunmadan Back ve Forward butonlarını kullanarak gezinebilir.

page ve query’nin nereden geldiğini önemli değildir. Bu bileşen görünürken, mevcut page ve query değerlerine göre ağdaki verilerle results’ı senkronize etmek istersiniz. Bu nedenle, bunu bir Efekt olarak kullanırsınız.

Ancak, yukarıdaki kodda bir hata bulunmaktadır. Hızlıca "hello" yazdığınızı hayal edin. Ardından query değeri "h"’den, "he", "hel", "hell" ve "hello" şeklinde değişecektir. Bu ayrı ayrı veri çekme işlemleri başlatacaktır, ancak yanıtların hangi sırayla geleceği konusunda garanti verilmemektedir. Örneğin, "hell" yanıtı "hello" yanıtından sonra gelebilir. setResults() çağrısı en son yapıldığından dolayı, yanlış arama sonuçlarını görüntülemiş olacaksınız. Buna “race condition” denir: İki farklı istek birbirleriyle “yarıştı” ve beklediğinizden farklı bir sırayla geldi.

Race condition sorununu düzeltmek, eski yanıtları görmezden gelmek için bir temizleme fonksiyonu eklemeniz gerekmektedir:

function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [page, setPage] = useState(1);
useEffect(() => {
let ignore = false;
fetchResults(query, page).then(json => {
if (!ignore) {
setResults(json);
}
});
return () => {
ignore = true;
};
}, [query, page]);

function handleNextPageClick() {
setPage(page + 1);
}
// ...
}

Bu, Efektiniz veri çektiğinde, en son istenen isteğin haricindeki tüm yanıtların görmezden gelinmesini sağlar.

Race conditionları yönetmek, veri çekme işlemini uygularken karşılaşılan tek zorluk değildir. Ayrıca yanıtların önbelleğe alınması (kullanıcının Back butonuna tıkladığında önceki ekranı anında görebilmesi için), sunucuda veri çekme işleminin nasıl gerçekleştirileceği (ilk sunucu tarafından oluşturulan HTML’in spinner yerine çekilen içeriği içermesi için) ve ağ gecikmelerinden kaçınma yöntemleri (bir alt elemanın, üst elemanların tamamlanmasını beklemeksizin veri çekme işlemi yapabilmesi) gibi düşüncelerde bulunmanız gerekebilir.

Bu sorunlar sadece React için değil, herhangi bir UI kütüphanesi için geçerlidir. Bunları çözmek kolay değildir, bu yüzden modern frameworkler verileri Efektlerin içerisinden çekmek yerine daha verimli yerleşik veri çekme mekanizmaları sunar.

Eğer bir framework kullanmadıysanız (ve kendiniz oluşturmak istemiyorsanız) ama Efektlerden veri çekme işlemini daha kolay şekilde yapmak istiyorsanız, kendi veri çekme mantığınızı bu örnekteki gibi özel bir Hook’a çevirin:

function SearchResults({ query }) {
const [page, setPage] = useState(1);
const params = new URLSearchParams({ query, page });
const results = useData(`/api/search?${params}`);

function handleNextPageClick() {
setPage(page + 1);
}
// ...
}

function useData(url) {
const [data, setData] = useState(null);
useEffect(() => {
let ignore = false;
fetch(url)
.then(response => response.json())
.then(json => {
if (!ignore) {
setData(json);
}
});
return () => {
ignore = true;
};
}, [url]);
return data;
}

Muhtemelen hata yönetimi ve içeriğin yüklenip yüklenmediğini takip etmek için muhtemelen biraz mantık eklemek isteyeceksiniz. Bu şekilde kendiniz bir Hook oluşturabilir veya React ekosisteminde mevcut olan birçok çözümden birini kullanabilirsiniz. Bu tek başına, bir framework’ün yerleşik veri çekme mekanizmasını kullanmak kadar verimli olmayabilir, ancak veri çekme mantığını özel bir Hook’a taşımak, daha sonra verimli bir veri çekme stratejisini benimsemeyi kolaylaştıracaktır.

Genelde, ne zaman Efekt yazmak zorunda kalsanız, useData gibi daha deklaratif ve amaç odaklı bir API’ye sahip olan özel bir Hook’a bir işlevselliği çıkarabileceğiniz durumları gözlemleyin. Bileşenlerinizde daha az sayıda useEffect çağrısı olduğunda, uygulamanızın bakımını daha rahat yapabileceksiniz.

Özet

  • Eğer render işlemi sırasında hesaplama yapabiliyorsanız, bir Efekte ihtiyacınız yoktur.
  • Masraflı hesaplamaları önbelleğe almak için, useEffect yerine useMemo kullanın.
  • Bir bileşen ağacının durumunu sıfırlamak için ona farklı bir key iletin.
  • Bir özelliğin değişimi sonucunda belirli bir state’in sıfırlanması için, bunu render sırasında ayarlayın.
  • Bir bileşen görüntülendiğinde çalışan kod, Efektlerde olmalıdır, geri kalan kodlar ise olaylarda yer almalıdır.
  • Eğer birkaç bileşenin state’ini güncellemeniz gerekiyorsa, bunu tek bir olay anında yapmak daha iyidir.
  • Farklı bileşenlerdeki state değişkenlerini senkronize etmeye çalıştığınızda, state’i yukarı taşımayı düşünün.
  • Veri çekmek için Effect’leri kullanabilirsiniz, ancak race conditionları önlemek için temizleme işlemini de uygulamanız gerekmektedir.

Problem 1 / 4:
Veriyi Efektler kullanmadan dönüştürün.

Aşağıdaki TodoList todoların bir listesini gösterir. “Show only active todos” checkbox’ı işaretlendiğinde, tamamlanmış todolar listede gösterilmez. Hangi todoların görünür olduğuna bakmaksızın, footer henüz tamamlanmayan todoların sayısını gösterir.

Bu bileşeni tüm gereksiz state ve Efektleri ortadan kalırarak basitleştirin.

import { useState, useEffect } from 'react';
import { initialTodos, createTodo } from './todos.js';

export default function TodoList() {
  const [todos, setTodos] = useState(initialTodos);
  const [showActive, setShowActive] = useState(false);
  const [activeTodos, setActiveTodos] = useState([]);
  const [visibleTodos, setVisibleTodos] = useState([]);
  const [footer, setFooter] = useState(null);

  useEffect(() => {
    setActiveTodos(todos.filter(todo => !todo.completed));
  }, [todos]);

  useEffect(() => {
    setVisibleTodos(showActive ? activeTodos : todos);
  }, [showActive, todos, activeTodos]);

  useEffect(() => {
    setFooter(
      <footer>
        {activeTodos.length} todos left
      </footer>
    );
  }, [activeTodos]);

  return (
    <>
      <label>
        <input
          type="checkbox"
          checked={showActive}
          onChange={e => setShowActive(e.target.checked)}
        />
        Show only active todos
      </label>
      <NewTodo onAdd={newTodo => setTodos([...todos, newTodo])} />
      <ul>
        {visibleTodos.map(todo => (
          <li key={todo.id}>
            {todo.completed ? <s>{todo.text}</s> : todo.text}
          </li>
        ))}
      </ul>
      {footer}
    </>
  );
}

function NewTodo({ onAdd }) {
  const [text, setText] = useState('');

  function handleAddClick() {
    setText('');
    onAdd(createTodo(text));
  }

  return (
    <>
      <input value={text} onChange={e => setText(e.target.value)} />
      <button onClick={handleAddClick}>
        Add
      </button>
    </>
  );
}