React ile Application State Yönetimi

Gizem Korkmaz
9 min readMar 27, 2022

--

Photo by Rene Böhmer

Bu yazı, Kent C. Dodds’un Application State Management With React adlı makalesinin bir çevirisidir.

State yönetimi, herhangi bir uygulamanın tartışmasız en zor kısmıdır. Bu yüzden bu kadar çok state yönetimi kütüphanesi var ve sayıları her geçen gün artıyor (Hatta bunların bazıları, diğerlerinin üzerine inşa edilmiş halde. Npm’de yüzlerce “kolay redux” abstraction’ı (soyutlaması) görebilirsiniz.). State yönetiminin kendisi başlı başına zor bir problem olmasına rağmen, onu bu denli zorlaştıran şeylerden birinin de problemin çözümüne yönelik uyguladığımız bazı gereğinden fazla teknik uygulamalar (over-engineering) olduğunu düşünüyorum.

React’i kullanmaya başladığımdan beri uygulamaya çalıştığım bir state yönetimi çözümü var ve React Hooklarının çıkması ile bu çözüm büyük ölçüde basitleşti.

Uygulamalarımızı oluştururken React bileşenlerinden sıklıkla lego parçalarıymış gibi bahsederiz ve insanlar bunu duyduklarında, state yönetiminin bu durumdan bir şekilde hariç tutulduğunu düşünüyorlar gibi. Benim state yönetimi sorununa uyguladığım kişisel çözümüm arkasındaki “sır”, uygulamanın state’inin, kendi ağaç yapısı ile nasıl eşleştiğini düşünmektir.

Redux’ın bu kadar başarılı olmasının nedenlerinden biri, react-redux’ın “prop drilling” problemini çözmesiydi. Bileşeninizi sihirli bir connectfonksiyonuna göndererek veriyi ağacınızın farklı bölümleri ile paylaşabilmeniz harika bir şeydi. Reducer/action/creator vs. de harika şeyler fakat redux’ın her yerde olmasının sebebininin, geliştiriciler için “prop drilling” sorununu çözmesinden kaynaklandığına inanıyorum.

Redux’ı yalnızca bir projede kullanmış olmamın sebebi de bu. Geliştiricilerin sürekli olarak tüm state’lerini redux’a koyduklarını görüyorum. Üstelik sadece global application state’leri değil, lokal state’leri de dahil ediyorlar. Bu, pek çok soruna neden olan bir durum. En basitinden, herhangi bir state etkileşimi sağladığınızda buna reducer, action creator/type ve dispatch call da dahildir ve bu da en sonunda birçok dosyayı açıp kodlarınızı adım adım incelerken neler olup bittiğini, tüm bunların kurduğunuz yapıda ne gibi etkiler yarattığını anlamaya çalışmanızla sonuçlanır.

Açık olmak gerekirse, bu gerçekten global olan state’ler için gayet işe yarar ancak daha basit state’ler için (örneğin bir modal’ın açık olup olmaması ya da form input’unun değerini tutan bir state gibi) büyük bir sorundur. Daha da kötüsü, bunlar pek de iyi ölçeklenemez. Uygulamanız büyüdükçe sorun da büyür. Tabii ki uygulamanızın farklı yerlerini farklı reducerlar ile yönetebilirsiniz ancak tüm bu action creatorlar ve reducerlar ile gittiğiniz dolaylı yol pek ideal değildir.

Tüm application state’lerinizi tek bir obje içerisinde tutmanız, redux kullanmasanız bile başka sorunlara yol açabilir. Bir React <Context.Provider>'ı yeni bir değer aldığında, bu değeri kullanan (consume) tüm bileşenler güncellenir ve verinizin yalnızca bir kısmı ile alakalı olan bir fonksiyon bileşeni olsa dahi tekrar render edilmek zorunda kalır. Bu da potansiyel performans sorunlarına yol açabilir. (React-redux v6, hooklar ile doğru çalışmadığını fark edene kadar bu yaklaşımı kullanmayı denedi. Bu da onları v7'de bu sorunları çözmeye yönelik farklı bir yaklaşım kullanmaya mecbur bıraktı.) Demek istediğim şu ki; statelerinizi daha akıllıca ayırırsanız ve react ağacında önemli oldukları yerlere yakın yerleştirirseniz bu sorunu yaşamazsınız.

İşin en can alıcı noktası burada; React ile bir uygulama geliştiriyorsanız zaten uygulamanızın içerisinde hali hazırda bir state yönetimi kütüphanesi kuruludur. Bunu kullanmak için npm install(ya da yarn add) yazmak zorunda da değilsiniz. Kullanıcılarınız için ekstra byte maliyeti yoktur ve npm üzerindeki tüm React paketleri ile entegre edilebilir. Üstelik React takımı tarafından da güzelce dokümanlaştırılmıştır. Bu, React’in ta kendisidir.

React, bir state yönetimi kütüphanesidir.

Bir React uygulaması geliştirdiğiniz zaman <App /> ile başlayan <input />, <div />, ve <button /> gibi elemenler ile biten bir ağaç oluşturmak için bir grup bileşeni bir araya getirirsiniz. Uygulamanızı oluşturduğunuz tüm düşük seviye composite bileşenleri tek bir merkezi konumda yönetemezsiniz. Bunun yerine, her bir bileşenin bunu yönetmesine izin verirsiniz ve bu, kullanıcı arayüzünü oluşturmanızda gerçekten etkili bir yol haline gelir. Bunu state ile de yapabilirsiniz. Muhtemelen zaten yapıyorsunuz da:

function Counter() {
const [count, setCount] = React.useState(0)
const increment = () => setCount(c => c + 1)
return <button onClick={increment}>{count}</button>
}

function App() {
return <Counter />
}

Bahsettiğim her şeyin class yapısı ile de çalıştığını unutmayın. Hooklar sadece işleri biraz daha kolaylaştırdı (özellikle de context konusunda, buna birazdan değineceğiz).

class Counter extends React.Component {
state = {count: 0}
increment = () => this.setState(({count}) => ({count: count + 1}))
render() {
return <button onClick={this.increment}>{this.state.count}</button>
}
}

“Tabii, Kent. Elbette tek bir bileşen içerisindeki tek bir elementi yönetmek kolay bir şey. Peki ya bileşenler arası paylaşmam gereken bir state olduğu zaman ne yapılacak? Örneğin, şunu yapmak isteseydim:

function CountDisplay() {
// `count` nereden geliyor?
return <div>The current counter count is {count}</div>
}

function App() {
return (
<div>
<CountDisplay />
<Counter />
</div>
)
}

count, <Counter />, bileşeni içerisinde yönetiliyor ve artık count değerine <CountDisplay /> bileşeninden erişebilmem ve <Counter />bileşeninde güncelleyebilmem için bir state yönetim kütüphanesine ihtiyacım var!

Bu sorunun çözümü en az React’in kendisi kadar eskiye dayanıyor (belki de daha eski?) ve kendimi bildim bileli dokümanlarında mevcut: State’i Yukarı Taşıma (Lifting State Up)

“State’i yukarı taşımak”, React’in state probleminin meşru bir çözümüdür ve son derece güvenilirdir. Bunu, mevcut duruma şu şekilde uyarlarız:

function Counter({count, onIncrementClick}) {
return <button onClick={onIncrementClick}>{count}</button>
}

function CountDisplay({count}) {
return <div>The current counter count is {count}</div>
}

function App() {
const [count, setCount] = React.useState(0)
const increment = () => setCount(c => c + 1)
return (
<div>
<CountDisplay count={count} />
<Counter count={count} onIncrementClick={increment} />
</div>
)
}

Sadece state’imizden kimin sorumlu olduğunu değiştirdik ve sonuç gayet açık. State’i uygulamamızın en tepesine kadar da çıkarabiliriz.

“Elbette öyle, Kent; peki ya prop drilling problemi ne olacak?”

Güzel soru. Buna karşı ilk savunmanız bileşenlerinizi oluşturduğunun yapıyı değiştirmek olmalı. Bileşen yapısının (component composition) avantajlarından faydalanabilirsiniz. Belki de şunun yerine:

function App() {
const [someState, setSomeState] = React.useState('some state')
return (
<>
<Header someState={someState} onStateChange={setSomeState} />
<LeftNav someState={someState} onStateChange={setSomeState} />
<MainContent someState={someState} onStateChange={setSomeState} />
</>
)
}

Bunu uygulayabilirsiniz:

function App() {
const [someState, setSomeState] = React.useState('some state')
return (
<>
<Header
logo={<Logo someState={someState} />}
settings={<Settings onStateChange={setSomeState} />}
/>
<LeftNav>
<SomeLink someState={someState} />
<SomeOtherLink someState={someState} />
<Etc someState={someState} />
</LeftNav>
<MainContent>
<SomeSensibleComponent someState={someState} />
<AndSoOn someState={someState} />
</MainContent>
</>
)
}

Bu çok açıklayıcı değilse (çünkü gerçekten çok zoraki uydurulmuş bir şey), Michael Jackson’ın ne demek istediğimi netleştirmeye yardımcı olması için izleyebileceğiniz harika bir videosu var.

Yine de, eninde sonunda composition’ın da işinize yaramadığı durumlar gelecek ve böylece bir sonraki adımınız React’in Context API’ını kullanmak olacak. Bu aslında uzun süredir var olan fakat “resmi olmayan” bir çözümdü. Dediğim gibi, birçok kişi react-redux’ı kullandı çünkü React dokümanlarındaki uyarı için endişelenmelerine gerek kalmadan bahsettiğim mekanizmayı kullanarak bu sorunu çözüyordu. Ancak artık contextresmi olarak React API’ının bir parçası olarak destekleniyor ve herhangi bir sorunla karşılaşmadan direkt olarak kullanabiliyoruz:

import * as React from 'react'

const CountContext = React.createContext()

function useCount() {
const context = React.useContext(CountContext)
if (!context) {
throw new Error(`useCount must be used within a CountProvider`)
}
return context
}

function CountProvider(props) {
const [count, setCount] = React.useState(0)
const value = React.useMemo(() => [count, setCount], [count])
return <CountContext.Provider value={value} {...props} />
}

export {CountProvider, useCount}
import * as React from 'react'
import {CountProvider, useCount} from './count-context'

function Counter() {
const [count, setCount] = useCount()
const increment = () => setCount(c => c + 1)
return <button onClick={increment}>{count}</button>
}

function CountDisplay() {
const [count] = useCount()
return <div>The current counter count is {count}</div>
}

function CountPage() {
return (
<div>
<CountProvider>
<CountDisplay />
<Counter />
</CountProvider>
</div>
)
}

Not: Verilen kod örneği ÇOK zoraki uyduruldu ve bu senaryodaki problemi çözmek için context kullanmanızı ÖNERMİYORUM. Lütfen, prop drilling’in neden özünde bir sorun teşkil etmediğini ve genellikle onun kullanılmasının daha uygun olduğunu anlamak için bu makaleyi okuyunuz. Context’e çok erken davranmayın!

Üstelik bu yaklaşımın harika bir yanı da, state güncellemesinin yaygın kullanımlarının tüm mantığını useCount hook’u içerisine koyabilmemizdir:

function useCount() {
const context = React.useContext(CountContext)
if (!context) {
throw new Error(`useCount must be used within a CountProvider`)
}
const [count, setCount] = context

const increment = () => setCount(c => c + 1)
return {
count,
setCount,
increment,
}
}

Ve bunu useState yerine useReducer olarak da kolayca değiştirebilirsiniz:

function countReducer(state, action) {
switch (action.type) {
case 'INCREMENT': {
return {count: state.count + 1}
}
default: {
throw new Error(`Unsupported action type: ${action.type}`)
}
}
}

function CountProvider(props) {
const [state, dispatch] = React.useReducer(countReducer, {count: 0})
const value = React.useMemo(() => [state, dispatch], [state])
return <CountContext.Provider value={value} {...props} />
}

function useCount() {
const context = React.useContext(CountContext)
if (!context) {
throw new Error(`useCount must be used within a CountProvider`)
}
const [state, dispatch] = context

const increment = () => dispatch({type: 'INCREMENT'})
return {
state,
dispatch,
increment,
}
}

Bu size muazzam derecede esneklik sağlar ve karmaşıklığı kat kat azaltır. İşleri bu şekilde yaparken hatırlamanız gereken birkaç önemli nokta:

  • Uygulamanızdaki her şeyin tek bir state objesi içerisinde olması gerekmiyor. Parçaları mantıksal olarak ayrı ayrı tutabilirsiniz (kullanıcı ayarlarının, bildirimlerle aynı context içerisinde olmasına gerek yok). Bu yaklaşım ile birden fazla provider’ınız olacaktır.
  • Tüm contextlerinizin global olarak erişebilir olmasına gerek yok! Statelerinizi mümkün olduğunca ihtiyaç olan yerlere yakın tutun.

İkinci maddeye ek olarak, uygulama ağacınız şuna benzer bir şekilde görünebilir:

function App() {
return (
<ThemeProvider>
<AuthenticationProvider>
<Router>
<Home path="/" />
<About path="/about" />
<UserPage path="/:userId" />
<UserSettings path="/settings" />
<Notifications path="/notifications" />
</Router>
</AuthenticationProvider>
</ThemeProvider>
)
}

function Notifications() {
return (
<NotificationsProvider>
<NotificationsTab />
<NotificationsTypeList />
<NotificationsList />
</NotificationsProvider>
)
}

function UserPage({username}) {
return (
<UserProvider username={username}>
<UserInfo />
<UserNav />
<UserActivity />
</UserProvider>
)
}

function UserSettings() {
// bu AuthenticationProvider ile alakalı bir hook olacaktır
const {user} = useAuthenticatedUser()
}

Her sayfanın, altındaki bileşenler için gerekli verileri içeren kendi provider’ına sahip olabileceğine dikkat edin. Kodu bölmek bu tür şeyler için de “işe yarar”. Veriyi her provider’a nasıl aldığınız, bu providerların kullandığı hooklara ve uygulamanızdaki verileri nasıl aldığınıza göre değişir ancak bunun provider’da nasıl çalıştığını bulmak için bakmaya nereden başlamanız gerektiğini bilirsiniz.

Bu düzenlemenin (colocation) neden faydalı olduğu hakkında daha fazla bilgi için “State Colocation React Uygulamanızı Daha Hızlı Hale Getirecek” ve “Colocation” adlı blog yazılarıma bir göz atın. Context hakkında daha fazla bilgi için ise “React Context Nasıl Etkili Şekilde Kullanılır” adlı yazımı okuyabilirsiniz.

Server Cache vs UI State

Eklemek istediğim son bir şey var. State’in pek çok kategorileri vardır fakat her state türü iki gruptan birine dahil edilebilir:

1- Server Cache (Sunucu Taraflı Önbellek) — Aslında sunucuda depolanan ve hızlı erişim için (kullanıcı verileri gibi) clientta tuttuğumuz state

2- UI State — Uygulamanın etkileşimli bölümlerini kontrol etmek için sadece kullanıcı arayüzünde işe yarayan state (modal’ın isOpen state’i gibi)

Bu ikisini bir araya getirerek hata yaparız. Server cache, UI state’inden doğası gereği farklı sorunlara sahiptir ve bu nedenle farklı şekilde yönetilmesi gerekir. Elinizde olan şeyin aslında state olmadığını, bir state cache’i olduğunu gerçeğini benimserseniz, o zaman doğru düşünmeye ve dolayısıyla doğru yönetmeye başlayabilirsiniz.

Bunu kesinlikle doğru useContext kullanımları ile kendi useState veya useReducer’ınız ile yönetebilirsiniz. Ancak doğrudan sonuca ulaşmanıza yardımcı olmak için caching işleminin oldukça zorlu bir problem olduğunu (ki bazıları bunun bilgisayar bilimlerinin en zor kısmı olduğunu söyler) ve bu konuda devlerin omuzlarında yükselmenin akıllıca olacağını söylememe müsaade edin.

Bu yüzden, ben bu tür state’ler için react-query’yi kullanıyorum ve öneriyorum. Biliyorum, biliyorum. State yönetimi için kütüphane kullanmanıza ihtiyaç olmadığını söylemiştim fakat react-query’yi tam olarak bir state yönetimi kütüphanesi olarak görmüyorum. Bunun bir cache olduğunu düşünüyorum. Hem de oldukça müthiş bir şey. Bir bakın derim! Tanner Linsley fena zeki biri.

Peki ya performans?

Yukarıdaki tavsiyelere uyduğunuzda, performans nadiren bir sorun haline gelir. Özellikle de colocation ile ilgili önerileri takip ettiğiniz zaman. Yine de performansın sorun teşkil edeceği bazı durumlar kesinlikle vardır. State ile alakalı performans sorunlarınız olduğu zaman, kontrol etmeniz gereken ilk şey, kaç bileşenin tekrar render edildiği ve bu bileşenlerin, bu state değişikliği nedeni ile gerçekten yeniden render edilmesine gerek olup olmadığını belirlemektir. Eğer gerek varsa, performans sorunu state’i yönetme mekanizmanızda değil, render etme hızınızdadır. Bu durumda da render hızınızı arttırmanız gerekmektedir.

Ancak, DOM güncellemeleri veya side effectleri olmadığı halde render edilen çok sayıda bileşen olduğunu fark ederseniz, o zaman bu bileşenlerin gereksiz yere render ediliyordur. Bu, React için her zaman geçerli olan bir sorundur ve normalde tek başına bir problem teşkil etmez (ve öncelikle gereksiz renderları daha hızlı hale getirmeye odaklanmalısınız). Ancak işleri gerçekten yavaşlatıyorlarsa kullanabileceğiniz, React context’te state ile yaşanan bazı performans sorunlarına çözüm yaklaşımları:

1- State’inizi tek bir depolama alanı yerine farklı mantıksal parçalara bölün, böylece state’in herhangi bir bölümündeki tek bir güncelleme, uygulamanızdaki her bileşende bir güncellemeyi TETİKLEMEZ.

2- Context provider’ınızı optimize edin

3- Jotai’yi devreye sokun

İşte, yeni bir kütüphane tavsiyesi daha. Doğru, React’in yerleşik state yönetimi abstractionlarının pek uygun olmadığı bazı durumlar vardır. Mevcut tüm abstractionlar arasında, bu tür durumlar için en umut verici olan kütüphane de jotai. Bu tür durumların neler olduğunu merak ediyorsanız, jotai’ın en iyi çözdüğü problem türleri şurada gerçekten harika şekilde açıklanmış: Recoil: State Management for Today’s React — Dave McCabe aka @mcc_abe at @ReactEurope 2020. Recoil ve jotai oldukça benzerler (ve aynı tür problemleri çözüyorlar) fakat kendi (sınırlı) deneyimime göre, ben jotai’ı tercih ediyorum.

Sonuç

Tekrar ediyorum, tüm bunlar class yapısı ile de yapabileceğin şeylerdir (hookları kullanmak zorunda değilsiniz). Hooklar işleri oldukça kolaylaştırdılar fakat bu düşünce yapısını React 15 ile de sorunsuz bir şekilde uygulayabilirsiniz. State’i olabildiğince lokal tutun ve context’i sadece prop drilling bir problem olmaya başladığında kullanın. İşleri bu şekilde yapmak, state etkileşimlerini sürdürmenizi kolaylaştıracaktır.

--

--