Neredeyse herhangi bir React kod tabanını açın ve aynı refleksi fark edeceksiniz. Bir geliştirici bir değeri takip etmesi gerektiğini anlar ve useState'e başvurur. Bir sayaç mı lazım? useState. Geçici bir input değeri mi? useState. Bir modal'ı açıp kapatacak bir boolean mı? useState. Çok geçmeden, tek bir bileşen, her biri render'lar arasında kalıcı olması gerekip gerekmediği belli olmayan küçük bir veri parçasını yöneten düzinelerce ayrı hook barındırır hale gelir. Sonuç; gürültülü bir kod, fazladan re-render'lar ve bir bileşen boyunca bozuk para gibi saçılmış state'lerdir.
Bu alışkanlık anlaşılabilir. useState, çoğumuzun öğrendiği ilk hook'tur ve işe yarar. Ancak bir şeyin çalışıyor olması, onun tam olarak yerine oturuyor olduğu anlamına gelmez. Her veri parçasını reaktif bir state olarak ele almak, bileşen büyüdükçe ortaya çıkan sorunlara yol açar.
Değişiyor Olması, State'e İhtiyacı Olduğu Anlamına Gelmez
Zaman içinde değişen her değişken useState içinde yer almalıdır denemez. Bazı değerler, halihazırda sahip olduğunuz başka bir şeyin yalnızca sonucudur. Eğer bir kullanıcının tam adını, sadece firstName ve lastName değerlerini birleştirdiğiniz için state içinde tutuyorsanız, artık iki farklı doğruluk kaynağınız (source of truth) var demektir. Parent bileşen yeniden render edildiğinde firstName güncellenirse, fullName state'iniz siz onu senkronize etmek için başka bir effect çalıştırmadığınız sürece güncelliğini yitirmiş (stale) olarak kalır. Bir senkronizasyon effect'ine ihtiyacınız yok. Bir türetilmiş değere (derived value) ihtiyacınız var.
const fullName = `${firstName} ${lastName}`;
Bunu render sırasında hesaplayın. Eğer türetme işlemi maliyetliyse, bunu memoize edin. Ancak kullanıcı, o tam adı parçalarından bağımsız olarak düzenleyemiyorsa, ona kendi useState hook'unu vermeyin.
Aynı kural filtrelenmiş listeler için de geçerlidir. Eğer hem allItems hem de filteredItems değerlerini state içinde tutuyorsanız, bakım yükünüzü iki katına çıkarmış olursunuz. Filtreleme işlemini render sırasında yapın. Kaynak diziyi ve filtre metnini state içinde tutun, ardından görünür listeyi türetin. Bu, filtrelenmiş listenin kaynakla asla senkronizasyon dışı kalmamasını garanti eder.
Bazı Değerler Asla Re-render Tetiklememelidir
useState, React'e bir şeyin değiştiğini ve DOM'un güncellenmesi gerekebileceğini söylemek için özel olarak vardır. Eğer bir değer değişiyor ancak UI'ın hiçbir parçası bu değişikliği önemsemiyorsa, useRef daha iyi bir araçtır.
Zamanlayıcılar (timers) ve aralıklar (intervals) bunun klasik örneğidir. setInterval ID'lerini state içinde tutmak, kullanıcı aralık ID'sini göremese bile, bir zamanlayıcıyı her başlattığınızda veya durdurduğunuzda bir re-render'a neden olur. Bir ref, React'e bildirmeden bu değeri tutar. Aynı mantık; önceki propları takip etmek, boyama (paint) işleminden önce DOM düğümlerini ölçmek veya özel bir hook için en son callback'i saklamak için de geçerlidir. Kendinize şunu sorun: Bu değerin ekranda görünmesi gerekiyor mu? Cevap hayırsa, muhtemelen useState'e ihtiyacı yoktur.
DOM düğümlerinin kendileri de ref'lere aittir. Bir DOM elementini state içinde saklayabilmenize rağmen, bunu yapmak ref callback'i çalıştıktan sonra bir re-render tetikler. Çoğu durumda, düğüme yalnızca emirsel (imperative) bir metod veya bir ölçüm için ihtiyacınız vardır, onu farklı şekilde render etmek için değil.
Boolean Tuzağı
İlgili UI meseleleri, her flag (bayrak) kendi hook'una sahip olduğunda kontrolden çıkma eğilimindedir. isLoading, isError ve isSuccess değerlerinin üç ayrı boolean olarak tanımlandığı bileşenler görürsünüz. Sorun şu ki, bu üç durum birbirinden bağımsız değildir. Eğer hem isLoading hem de isSuccess true ise, UI'ınız imkansız bir durumdadır; ancak TypeScript ve React buna rağmen render etmenize izin verecektir.
İlgili state'leri gruplandırmak bu geçersiz kombinasyonları önler. Üç ayrı boolean yerine, tek bir durum dizesi (status string) takip edin: 'idle', 'loading', 'success' veya 'error'. Aynı anda yalnızca biri aktif olabilir, bu da imkansız durumları tip seviyesinde ortadan kaldırır. Veri daha karmaşıksa, bir discriminated union içeren bir nesne işleri daha da kolaylaştırır. Kendinizi aynı event handler içinde birkaç farklı useState çağrısını güncellerken buluyorsanız, bu o değerlerin birlikte olması gerektiğine dair bir sinyaldir.
Başka Bir useState Yerine useReducer'a Başvurun
State güncellemelerinin bir kovalamacaya dönüştüğü bir nokta vardır. Tek bir fonksiyon içinde önce setA, sonra setB, ardından koşullu olarak setC çağırırsınız. Bu kodu okuyacak bir sonraki geliştirici, bileşenin gerçekte ne yaptığını anlamak için tüm bu diziyi takip etmek zorunda kalır.
useReducer burada parlar. useState'in yerini daha gelişmiş olduğu için değil, mantık bunu gerektirdiği için alır. Bir reducer, state değişimlerini merkezileştirir. Event handler'lara emirsel komutlar serpiştirmek yerine, bir niyet (intention) gönderirsiniz: dispatch({ type: 'submitted' }). Bir sonraki state'in nasıl görüneceğine reducer karar verir. Bu, state mantığınız saf bir fonksiyon (pure function) olduğu için test etmeyi çok kolaylaştırır. Ayrıca her değişiklik izlenebilir bir aksiyon bıraktığı için hata ayıklamayı (debugging) da kolaylaştırır.
Bir reducer kullanmayı haklı çıkarmak için Redux'a ihtiyacınız yoktur. Birlikte güncellenen üç veya daha fazla state değişkeniniz varsa veya bir sonraki state'iniz bir öncekine büyük ölçüde bağlıysa, bir reducer bileşeni ciddi oranda basitleştirir.
State Aslında Nerede Yaşar
Bazen sorun state'i nasıl sakladığınız değil, nerede sakladığınızdır. Yaygın bir hata, sadece başka bir yerde ihtiyaç duyulabileceği gerekçesiyle state'i bir üst bileşene (parent) taşımaktır. Eğer bir state parçasını sadece tek bir uç bileşen (leaf component) kullanıyorsa, onu orada tutun. Bu colocation'dır ve değişikliklerin etki alanını azaltır. Bir çocuk bileşen açıldı diye üst bileşenin yeniden render edilmesine neden olmayın.
