React Proje Mimarisi
React ile uzun bir süredir uygulama geliştiriyorum ve gittikçe de daha çok seviyorum. React, uygulama mimarisi ve planı çıkartmak için gerçekten muazzam bir kütüphane. Temel yazılım prensiplerini (SOC gibi, SOLID gibi…) rahatça uygulayabileceğiniz ve ölçek büyüse de temiz bir codebase’e sahip olabileceğiniz bir yapı kurma imkanı sunuyor. Özellikle hook’lardan sonra tadından yenmeyecek bir kullanıma kavuşmuş durumda. Ben de bu yazıda React ile nasıl bir proje yapısı ve mimarisi kurabilirsiniz biraz bahsetmek istiyorum. Bunu hem best practice hem React temelleri karma bir yazı gibi düşünebilirsiniz. Anlatacaklarım elbette bir kural vs. değil, siz canınız nasıl isterse öyle ilerleyebilirsiniz, benimki sadece birer fikir versin istiyorum :)
Yazı biraz uzun olacak fakat yararı olacağını düşünüyorum. Ek olarak React Native üzerinden anlatım yapacağım ama birebir web ortamında da benzer bir düzeni düşünebilirsiniz.
Hazırsanız başlayalım! 🤟
Not: Normalde makale hazırlarken Türkçe kullanımına elimden geldiğince dikkat ediyor, İngilizce ya da başka bir dil ile yazıyı katletmemeye özen gösteriyorum. Fakat bu yazıda teknik terimleri (component, reusable, context vs.) gibi olduğu gibi kullanmak istiyorum. Çünkü kelimelerin Türkçe karşılığı içerik ile tam uymayabiliyor ve yeni başlayan arkadaşların teknik terimlere göz aşinalığı olsun istiyorum. Onun için şimdiden bilginiz olsun dostlar ✌️
Navigasyon
Navigasyon yapısı geliştireceğiniz uygulamanın bel kemiğidir. Ne kadar temiz ve dengeli bir yapı kurarsanız yeni isterler, yeni sayfalar geldiğinde o kadar rahat edersiniz ve ekleme yapacağınız zaman “Nereye, nasıl dahil edeyim?” sorusuna çok kafa yormamış olursunuz.
Bir uygulama geliştireceğiniz zaman tasarım safhasında tüm dizayn mimarisi de ortaya konmuş olur. Hangi ekranlar olacak, hangi amaca hizmet edecek, uygulamada sayfalar neye göre gruplanacak gibi sorular cevaplanmış olur ve siz bu noktada yönlendirme mimarisini de kurmuş olursunuz. Tüm bu mimariyi ekran tasarımlarına bakarak kurabilirsiniz.
Eğer uygulamanız birbirinden amaç olarak farklı sayfalara sahipse bunları ayrı birer Stack mimarisinde toplayabilirsiniz. Örneğin uygulama profil, mesajlaşma, ana sayfa gibi ana modüllere sahipse;
- App
- ProfileStack
- MessageStack
- TimeLineStack
...
...
...- ProfileStack
- ProfilePage
- UpdatePreferencesPage
- AddNewPhotoPage- MessageStack
- InboxPage
- NewMessagePage
- TrashCanPage- TimelineStack
- TimelinePage
- PostPage
- CommentsPage
- LikesPage
şeklinde bir yapı kurabilirsiniz.
Ana navigatör Profile, Message ve Timeline stacklerine sahip. Böylece uygulamamızın ana modülleri belirli ve birbirinden ayrılmış, alt ekranlara sahip. Misal; MessageStack modülü yalnızca mesaj bölümü ile alakalı ve yarın öbür gün yeni bir ekrana ihtiyaç duyarsa sadece o bölüme eklemek işimizi görecektir. İstediğimiz gibi bir ekrandan bir ekrana geçebiliriz. react-navigation bize bu noktada sınırsız özgürlük sunuyor, yeter ki biz planlamamızı iyi yapalım.
İç içe ne kadar stack vs. ekleneceğinin bir sınırı yok. Bağlam olarak birbirine çok yakın modüller birer stack yapısında toplanabilir. Örneğin uygulamanın güvenlik sayfasında bildirim kısmı iç içe 3–4 sayfadan oluşuyorsa bunu bir stack yapısında toplayabilirsiniz. Çünkü Settings stack’i altında NotificationPreferences, NotificationDetail, BlockedAppNotifications adında sayfalar görmek pek sağlıklı olmayacaktır. Çünkü bu sayfalar başlı başına Notifications stackine ihtiyaç duyuyorlar. Ayrıca onları o şekilde kullanmak yarın öbür gün başka ek sayfaları da o gruplamadan direkt oraya koymak demektir. Sonuçta geliştirmede belli bir yönteme sadık kalınmalı. Ya yarın öbür gün 10 sayfalık bir modül gelirse?
Bir proje; ya sabit bir geliştirme yolu izlememekten ya da yanlış geliştirme yolu izlemekten dolayı ölür.
Componentler
Bir modül geliştirdiğinizde gittikçe karmaşıklaşan veya tekrar kullanıma açık yapılar ayrı birer component olarak tasarlanmalıdır.
React ile bir sayfa ya da bir modül geliştirirken mümkün olduğunca parçalama yapmaya çalışın. React size bu imkanı verir ve sonuna kadar kullanmalısınız. Mevcut uğraştığınız component bugün oldukça sade bir tasarımda gözükebilir, buna ihtiyaç duymayabilirsiniz fakat sizden sonra ilgilenecek kişi geliştirmeye sizin gibi devam ederse, o component 200–300 satırları bulursa revizesi geliştirmeden çok, çok daha uzun sürecektir. Tuvalet misali; nasıl bulmak istiyorsanız, öyle de bırakmalısınız
Peki ne zaman bir componenti ayrıştırmak gerekir?
Bir uygulamanın tasarımı oluşturulurken göze hitap etmesi için sabit bir tasarım prensibi belirlenir. Butonlar, inputlar, modallar vs. hep tutarlı bir tasarıma sahiptir ve birbirlerine benzerler. On farklı buton tasarımı yerine tek bir buton tasarımının on adet varyasyonunu görürsünüz. Bu tutarlılıktır, kullanıcının aklında ve göz hafızasında uygulamanın bir imzasını oluşturur ve siz de projede kullanacağınız componentleri oluştururken bu tasarımlardan yola çıkarak tutarlı bir component mimarisi kurarsınız.
Örneğin; bir çok sayfada sürekli kullanılan bir buton tasarımı mevcutsa kendi buton componentiniz ve onun varyasyonlarını oluşturup, genel components dizininde saklayabilirsiniz. Başka bir sayfada kullanılmayan fakat ileriki safhalarda ihtiyaç duyabileceğiniz bi component tasarımı mevcutsa ve reusable (tekrar kullanılabilir) bir şekilde oluşturabiliyorsanız, yine genel components bölümünde o componenti saklayabilirsiniz.
Fakat tek bir ekranda kullanılacak componentler varsa onları o dizinin içinde saklamanız daha doğru olur. Bir örnek verelim;
Elimizde bir rapor analiz ekranı tasarımı olsun ve o ekranda bir grafik, bir tablo ve liste modalı ile button bileşenleri olsun. (Liste modalını FlatList + Modal bileşenlerinden oluşturduğumuz re-usable bir component olarak düşünelim)
Analiz ekranındaki grafik ve tablo componentleri sadece ve sadece analiz ekranında kullanılacak ve tamamen o logic’e bağlı kalacaksa o dizinin altında saklanması daha doğru olur. Çünkü birbirine ihtiyaç duyan yapılar birbirine yakın olmalıdır. Fakat liste modalı ve buton componentlerini genel dizinden çekmek daha doğru olacaktır. Bu amaç için üretildiler.
O halde dosya dizinimiz;
- components
- Button
- Button.tsx
- Button.style.ts
- Button.test.tsx
- Button.stories.tsx
- index.ts
- ListModal
- ListModal.tsx
- ListModal.style.ts
- ListModal.test.tsx
- ListModal.stories.tsx
- index.ts
...
...
- pages
- Analyze
- components
- AnalyzeGraph
- AnalyzeGraph.tsx
- AnalyzeGraph.style.ts
- AnalyzeGraph.test.tsx
- AnalyzeGraph.stories.tsx
- index.ts
- AnalyzeDataTable
- AnalyzeDataTable.tsx
- AnalyzeDataTable.style.ts
- AnalyzeDataTable.test.tsx
- AnalyzeDataTable.stories.tsx
- index.ts
- Analyze.tsx
- Analyze.style.tsx
- index.ts
şeklinde olacaktır. Analiz modülü ile alakalı ve sadece ona hizmet edecek componentler o modülün yakınında yer alıyor.
Not: İsimlendirirken ait oldukları modülün adını ön ek olarak vermek şahsen daha doğru olacaktır. Çünkü başka bir yapıda tamamen bambaşka bir grafik ya da veri tablosu componentine ihtiyaç duyabilirsiniz ve eğer sadece DataTable ismini kullanırsanız; yarın öbür gün proje büyüdüğünde belki de on tane aynı isimde bileşeniniz olacak ve aramaya kalktığınızda sizi daha çok uğraştıracaktır.
İkinci bir yol: stillendirme aşaması
Temiz kod yazmanın en temel prensibi değişkenlere ve değerlere doğru isimler vermektir. Stiller de bizim için birer değerdir ve doğru isimlendirilmelidir. Componentler için stil yazdığınızda onlara ne kadar doğru isimler verirseniz, o kadar maintain edilebilir bir kod yazmış olursunuz. Çünkü güncelleyecek kişi stil isminden hangi componente gideceğini rahat bulacaktır.
Component geliştirirken still isimlendirmesi aşamasında eğer çok fazla tekrar eden ön ek kullanıyorsanız, o stili yazdığınız bölümü custom componente çevirmeniz gerekiyor demektir.
Yani UserBanner.style.ts dosyanız şu şekildeyse;
contanier: {...},
title: {...},
inner_container: {...},
avatar_container: {...},
avatar_badge_header: {...},
avatar_title: {...},
input_label: {...},
Avatar.tsx şeklinde bir component’e ihtiyaç duyacağınızı hissedebilirsiniz. Çünkü stil aşamasında bir gruplaşma söz konusuysa belli ki gittikçe büyüyen bir yapı oluşmak üzeredir. Bunun için 3 ya da 5 tekrar eden stil ismi olma gibi bir şartı yok, yazarken o yapıyı takip edip kendiniz çıkarım yapabilirsiniz.
Ek olarak; her component illa bir logic barındırmalı diye bir kaide yok. Ne kadar parçalarsanız o kadar daha rahat kontrol edebilirsiniz kodunuzu ve testini de ayrıca yazma şansınız bulunacaktır.
Ufak bir yol tavsiyesi olmuş olsun bu da 🧳
Hooks
Yaşam döngüsünde belli bir görevi olan ve kendi başına ayrı bir iş mantığını temsil eden yapılar birer hook olarak soyutlanmalıdır. Bunun için bu yapıların kendi başına bir logic’e sahip olması (belirli bir iş bütününü temsil etmesi) ve tanımda da belirtildiği üzere lifecycle bütününde yer alması gerekir.
Bunun asıl amacı ilgili componentte ve genel anlamda geliştirdiğiniz yapıdaki yük dağılımını azaltmak ve reusable bir yapı kurabilmektir. Biz nasıl custom componentleri hem tekrar tekrar kullanabilir olmaları hem de kod karmaşasını azaltmaları için kullanıyorsak; custom hooklar da aynı amaçla oluşturulabilir. Önemli olan şey kurulan yapının doğru çalışması ve bundan emin olunması.
Bir custom hook’a ihtiyaç duyduğumuzu nasıl anlarız?
Bunu bir örnekle açıklayalım;
Proje kapsamında bir arama yapısına ihtiyaç duyduğunuzu düşünün. Uygulamanın bir çok yerinde kullanılabilecek, fuse.js paketini kullanacak bir SearchBox componenti misali bir yapıya ihtiyacınız var. Arama yapısını, örnekleyeceğimiz iki component’e uygulayalım önce.
(Kodları fazla uzatmadım fakat noktalı bölümleri ilgili compoenentin kendine has kısımları olarak düşünebilirsiniz)
Componentlerimize baktığımızda dikkatimizi çeken ilk şey ikisinde de aynı arama yapısının uygulanmış olması ve kod tekrarına girilmesi. Eğer bir yapıda çok fazla kod tekrarı yapılıyorsa emin olun bir şeyler ters gidiyor demektir.
Buna ek olarak kodu okuyan bir kişi herhangi bir dosyayı açtığında, o dosyanın adı neyse içinde de yalnızca ve yalnızca o isimle alakalı kodlar görmek isteyecektir. Siz CommentsScreen.tsx dosyasını açtığınızda o dosyanın içinde sadece yorumlar ekranı ile alakalı kodları görmek istersiniz. O kodun içinde ayrı bir logic taşıyan, kümelenmiş bir kod bloğu görmek istemezsiniz. Evet, örnekteki arama yapısı yine Product ve Member bileşenleri için kurulmuş ve onları ilgilendiriyor. Fakat o yapı kendi kendine bir mantık bütünü barındırıyor artık ve daha da ötesi, reusable bir formata çevrilebilir. İşte biz bu sebeplerden ötürü custom hook, component vs. yapısını kurma ihtiyacı duyuyoruz.
Örneğe dönecek olursak; arama işlemi için belirli bir state kullanımı var ve ayrıca yaşam döngüsünün içinde yer alan bir sistem mevcut. Kullanıcı arama kutusuna yazdıkça bu string searchKey statetinde tutuluyor ve bu state güncellendikçe ana liste de filtreleniyor.
Peki biz bu yapıyı nasıl daha iyi kurabiliriz?
Arama işlemi için kullandığımız yapıları useSearch adını verdiğimiz bir hook içerisinde toplayabiliriz. Öyle bir hook oluşturalım ki herhangi bir modüle bağlı olmasın, reusable bir yapıda olsun ki ne zaman istersek özgürce kullanabilelim.
Fuse.js modülünü kullanacağımız için oluşturacağımız hookta data ve arama kriterlerimizi input olarak gönderebilir, arama sonucunu ve tetikleyeceğimiz arama metodunu output olarak dönebiliriz.
O halde oluşturacağımız hook;
şu şekilde olacaktır.
TypeScript sayesinde ile artık hookumuz type destekli bir yapıda. Bu sayede istediğimiz tipi geçirerek geri dönüşü de o formatta alabiliyoruz. Hook’un iş akışı da demin bahsettiğimiz şeklin aynısı zaten, kodu okuyunca fark edersiniz.
Bu custom hook’u componentlerimizde kullanmak istersek de;
Görüldüğü üzere artık ilgili componentlerimizden arama yapısı soyutlanmış durumda. Hem kod kalabalığı azaltılmış oldu hem de ne zaman bir search yapısına ihtiyaç duyarsak elimizin altında hazır bir custom hook’umuz var.
Bu sayede daha temiz ve test edilebilir bir yapı kurduk.
Bu arada dediğim gibi; hooklar da aynı componentler gibi belli bir yapıya bağlı ya da generic oluşturulabilir. Bu örneğimizde genel kullanıma uygun bir useSearch hook’u yazdık, fakat belirli bir content’e bağlı özel hooklar da yazabilirsiniz. Örneğin belirli bir sayfanın veri çekme ve düzenleme işlemi için o sayfanın özel hook’unu oluşturup yapıları birbirinden soyutlayabilirsiniz.
Yani;
- hooks
- useSearch
- useSearch.ts
- useSearch.test.tsx
- index.ts
...
...
- pages
- Messages
- hooks
- useMessage
- useMessage.ts
- useMessage.test.tsx
- index.ts
- useReadStatus
- useReadStatus.tsx
- useReadStatus.test.tsx
- index.ts
- Messages.tsx
- Messages.style.tsx
- index.ts
useSearch proje kapsamında kullanılan generic bir hook iken; useMessage mesaj verilerini çekmekten sorumlu, useReadStatus ise mesajın okundu bilgisini dinleyen bir subscriber hook’u görevinde. Aynı componentlerdeki mantık geçerli.
Hooklar’ımız da bu şekilde 🔗
Clean Code kitabında bir fonksiyonun single-responsibility gerekliliği için çok güzel bir tanım mevcut. Onu React yapısına uyarlarsak: Bir component ya da bir hook; tek bir işi yapmalı, o işi çok iyi yapmalı ve yalnızca o işi yapmalı.
Context
Birbirleriyle direkt olarak haberleşemeyen fakat içerik olarak birbirleriyle bağımlı her bir modül için ayrı birer context yapısı kurmalısınız.
Context’in tam kelime anlamı içerik, konu demektir ve sadece uygulamayı saran tek bir yapı olarak düşünmemek gerekir. Uygulama karmaşıklaştıkça mantık olarak birbiriyle yakın yapılar çoğalır ve bu yapıların birbirlerinden ayrılması gerekir. Context; bu yapıların kendi içlerinde haberleşmesini sağlama görevini üstlenir. Örneğin; mesajlaşma modülünde sayfalar ya da componentler arası haberleşmeye ihtiyacınız varsa MessagesContext yapısını kurup yalnızca mesajlar modülünü kapsayarak diğer bölümlerden bağımsız bir iş mantığı kurabilirsiniz. Aynı uygulamada yakın çevredeki arkadaşları bulmak istediğiniz Nearby bölümünüz varsa ve birden fazla sayfadan ve iş parçasından oluşuyorsa NearbyContext yapısı ile bunları diğerlerinden soyutlayabilirsiniz.
Peki uygulamanın her yerinden erişilebilen bir global bir yapıya ihtiyaç varsa uygulamayı tek bir context ile saramaz mıyız?
Tabii ki de yapabilirsiniz.
Global state management zaten bunun için var. Burada asıl dikkat etmeniz gereken şey tek bir contexte aşırı yük bindirmemeniz gerektiği. Tüm uygulamayı AppContext diye bir şeyle sarıp içine hem gelen mesajları, hem tema bilgisini, hem de kullanıcı bilgisini koymamalısınız. Çünkü bu saydıklarım için ayrı iş bölümleri zaten oluşturdunuz ve birbirinden bağımsız yapılar oldukları aşikar.
Ek olarak; context, içinde tuttuğu herhangi bir state güncellemesinde bağlı olduğu her bir componenti günceller.
Örneğin; siz AppContext’te member ve messages diye stateler oluşturdunuz ve Profile.tsx’te yalnızca member statetini, MessageList.tsx’te yalnızca messages stateini dinliyorsunuz. Siz, yeni bir mesaj gelip messages state’tini güncellendiğinizde Profile sayfası da render alacaktır. Çünkü kendisi AppContext’tini dinlemekte ve ilgili olduğu (aslında olmadığı) context’te bir güncelleme var. Sizce gelen mesajlar ile profil güncelleme sayfasının bir ilişkisi olabilir mi? Neden yeni mesaj geldiğinde profil sayfası update olsun ki? Bu gereksiz bir render demektir ve bunlar çığ gibi büyüdüğünde uygulamada performans sorunları ortaya çıkacaktır.
İşte bu sebepten ötürü her bir içerik için ayrı bir context oluşturup birbirinden uzaklaştırmalısınız ve mantık bütününü korumalısınız. Daha da öte bir sebebi, uygulama bakım safhasına girdiğinde herhangi bir modülde güncelleme yapacak kişi rahatça context’i seçebilmeli ve kurulan mimariyi anlayabilmeli. Aslında burada clean code’un en temel öğretisi tekrar devreye giriyor; demin de bahsettiğimiz doğru değişken isimlendirme.
Siz kuracağınız context’i doğru şekilde isimlendirdiğinizde kuracağınız yapı da sağlıklı ilerleyecek. Çünkü UserContext’i gören bir kişi kullanıcı bilgilerini buradan alacağını bilir, global ve kullanıcı ile alakalı bir state bilgisini buraya eklemesi gerektiğini bilir, ayarlar ile alakalı bir state’e ihtiyacı varsa kalkıp buraya eklemez. Clean code prensipleri işte bu yüzden önemli.
Ayrıca vakti zamanında kullanıcılar Context API’ın aynı Redux gibi sadece çağrılan değişken güncellendiğinde bağlı olduğu component’i güncellesin diye issue açmıştı ve Dan Abramov’un şu cevabı aslında context’in çalışma mantığını çok güzel özetliyor.
Context’i dinelyen componentler zaten o context’e ihtiyaç duyuyor olmalı. Eğer çağırdığınız herhangi bir state’i gereksiz görüyorsanız; ya o state’tin context’e işi yok ya da context’i yanlış kurgulamışsınız demektir. O noktada context’leri ayırmanız gerekiyor olabilir. Bu tamamiyle sizin kurduğunuz mimariyle alakalı olacaktır.
Context kullandığınız zaman her daim çağırdığınız statelerin gerçekten o component’te ihtiyaç duyduğundan emin olun. Hata yapma ihtimaliniz düşecektir.
Ufak bir örnek olarak da;
[ App.tsx ]<AppProvider> (member, memberPreferences, messages, language)
<Navigation />
</AppProvider>
Yerine ayrıştırma yaparsak;
[ App.tsx ]<i18nProvider> (language)
<MemberProvider> (member, memberPreferences)
<Navigation />
</MemberProvider>
</i18nProvider>...
...
...[ MessageStack.tsx ]<MessagesProvider> (messages)
<Stack.Navigator>
<Stack.Screen .../>
<Stack.Screen .../>
<Stack.Screen .../>
</Stack.Navigator>
</MessagesProvider>
şeklinde kullanmak çok daha iyi olacaktır. Tahmin ettiğiniz üzere MessagesProvider ayrıştırdık ama entry point’e yerleştirmedik. Çünkü i18n ve Member providerlar’ı her yerden kullanıma ihtiyaç duyulmuş fakat Messages sadece mesaj kapsamında kullanılacak ve sadece o kısımda güncellemeyi tetikleyecek. E zaten mesaj context’inin mesaj bölümünü güncellemesi de zaten doğal ve beklenen bir şey.
Sonuç
React’ın can damarı konuları kendimce biraz anlatmaya çalıştım. Umarım siz okuyucular için güzel bir kaynak ve makale olmuştur.
Yazının başında da dediğim gibi, React bu tarz mimariler kurmak için gerçekten harika bir kütüphane. Temiz ve titiz çalışmak istediğinizde size olabildiğince imkan sunuyor. Güzel kurulmuş bir proje sistemi ile gayet performanslı ve kullanışlı web/mobil uygulamalar ortaya koyabiliyorsunuz.
İçerik ile ilgili yorumlarınız olursa da almak isterim.
Gelecek yazılarda görüşmek üzere dostlar, kendinize çok iyi bakın, güzellikle kalın ✌️
🎙
“Clean code always looks like it was written by someone who cares”
― Michael Feathers