Bir Efekte İhtiyacınız Olmayabilir

Efektler, React paradigmasından bir kaçış yoludur. Bu kaçış yolları size React’tan “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
  • Hangi mantık olay yöneticilerine taşınmalı
  • Üst bileşenlere 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’den 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() yavaş 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”):

import { useMemo, useState } from 'react';

function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
const visibleTodos = useMemo(() => {
// ✅ todos veya filter değişmeden yeniden çalıştırmayın.
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('');
// ✅ getFilteredTodos() fonksiyonunu todos veya filter değişmeden yeniden çalıştırmayın.
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 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 childrenlar 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ırlanacaktı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’tan 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 child bileşeni 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 child bileşenlerinin 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 henüz List bileşeninin childrenını render etmemiştir veya DOM henüz güncellenmemiştir, bu sebeple, List bileşen childrenları 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 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 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 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 işleyicisinden çağrılan bir fonksiyon içine yerleştirin:

function ProductPage({ product, addToCart }) {
// ✅ Olaya özgü mantık, olay işleyicilerden ç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 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, olay işleyicilerden ç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 işleyicisinde 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 Etki 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 childrenları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 bileşen 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 yönticilerden ç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

Some logic should only run once when the app loads.

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 geliştirme ortamında 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.

Parent bileşenlerini 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 parent bileşene bildirimde bulunmak istiyorsunuz, böylece bir onChange olayını bir Efektten çağırıyorsunuz:

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

// 🔴 onChange işleyicisi ç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, parent bileşenden iletilen onChange fonksiyonunu çağıran Effect’i çalıştırır. Şimdi parent bileşen, 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çindeki 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 parent bileşeni, 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 parent bileşenden isOn değerini alabilirsiniz:

// ✅ Also good: the component is fully controlled by its parent Bileşenin, kendi parent bileşeni 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” parent bileşenin kendi state’ini değiştirerek, parent bileşenin Toggle‘ı tamamen kontrol etmesine olanak tanır. Bu, parent bileşenin 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!

Parent’a 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 parent bileşene iletmekten kaçının.
useEffect(() => {
if (data) {
onFetched(data);
}
}, [onFetched, data]);
// ...
}

React içerisinde, veri akışı parent bileşenlerinin children’larına 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. Child bileşenler, parent bileşenlerinin state’ini Efektler içerisinde güncellediği durumlarda, veri akışını takip etmek zorlaşabilir. Parent ve child bileşeninin aynı veriye ihtiyacı olduğunda, parent bileşeninin ihtiyaç duyduğunuz veriyi çekmesini sağlayın ve child bileşenlerine doğru veriyi aşağıya iletin:

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

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

This is simpler and keeps the data flow predictable: the data flows down from the parent to the child. : veri akışı

Harici veri depolarını takip etme

Bazen bileşeninizin, React state dışındaki bazı verileri takip etmesi gerekebilir. 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 bileşenin her üst bileşenin 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 için bazı mantık ve içeriğin yüklenip yüklenmediğini takip etmek için bir mantık eklemek isteyebilirsiniz. Bu şekilde kendiniz bir Hook oluşturabilir veya React ekosisteminde mevcut olan birçok çözümden birini kullanabilirsiniz. Bunun 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 Efektleri yazmak zorunda kaldığınızda, 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>
    </>
  );
}