React.js Tasarım Desenleri(2024)

Eyüp Özmen
9 min readFeb 27, 2024

(React.js Design Patterns in 2024)

Frontend geliştiriciliği tanımlayacak olsak; uygulama verisinin kullanıcı deneyimini en iyilecek şekilde yönetilmesini sağlayan arayüzler geliştirmek şeklinde bir ifade ortaya çıkar kuşkusuz.

Buradan hareketle hem veri(Data) hem de kullanıcı deneyimi(UX) React özellinde bileşenlerin(UI) doğru şekilde yönetilmesini gerektirir. Çeşitli en iyi pratikler olarak adlandıracağımız paternlerin ortaya çıkarılması bileşenler arası veri iletişimin sağlanmasını temel alır ve hedeflenen en az maliyetle bu iletişimin sağlanması amaçlanır.

React, günümüz web uygulamalarının temel taşlarından biri haline geldi. Ancak, büyük ve karmaşık React uygulamaları geliştirirken, kodun düzenli tutulması ve performansın optimize edilmesi de oldukça önemlidir. Bu yazıda, React geliştiricileri için temel tasarım desenlerini inceleyeceğiz ve nasıl kullanılabileceklerine dair pratik bilgiler sunacağız.

Neden tasarım desenlerine ihtiyaç duyarız?

  1. Geliştiriciler için ortak bir şema ortaya koyarlar.

Kimi zaman daha önce karşılaşılmamış problemler ile yüzleşmek zorunda kaldığımızda ve en önemlisi bunu bir ekip halinde gerçekleştirmemiz gerektiğinde işbirliğini sağlayacak ortak noktalara ihtiyaç duyarız. Böyle bir durumda temel bir şema üzerinden çözüme ulaşmayı hedeflemek hem daha kolay ve hem de işbirliğini mümkün kılacaktır.

2. React en iyi uygulamalarının(best practices) ortaya çıkarılmasını mümkün kılarlar.

Araştırılmış, uygulanmış, test edilmiş, dökümante edilmiş bir çözüm, bizi sonuca ulaşmak açısından daha rahat hissettirecektir.

Yaygın kullanıma sahip tasarım desenleri olmakla birlikte burada sayı ve sınır belirlemek oldukça zor dolayısıyla önemli gördüğüm aşağıda sıralı 5 tasarım desenini kod örnekleri ile irdelemeye çalışacağım.

1- Container/Presentational Pattern

2- HOCs Pattern

3- Render Props Pattern

4-Compound Component Pattern

5-Hooks Pattern

1. Container/Prensentational Pattern

Ana odağına SOLID prensiplerinin ilki olan Single Responsibility(Tek Görevlilik) ve Separation of Concern(Problemlerin Ayrıştırılması) ilkesini alan bu yaklaşım; fonksiyonel bileşenlere Hook yapılarının entegre olması nedeniyle propülerliğini yitirse de kullanım mantığını işlemekte fayda görüyorum.

Container bileşenleri, veri alma ve işleme gibi mantıksal görevleri üstlenirken, sunum bileşenleri, UI’yi oluşturmak ve kullanıcıyla etkileşimde bulunmakla ilgilenir. Bu desen, bileşenler arasında sorumlulukların net bir şekilde ayrılmasını sağlar ve kodun daha okunabilir ve bakımı daha kolay hale gelmesine yardımcı olur.

// Container Component
const UserListContainer = () => {
const [users, setUsers] = useState([]);

useEffect(() => {
fetchUsers().then((data) => setUsers(data));
}, []);

const handleDeleteUser = (userId) => {
deleteUser(userId).then(() => {
setUsers(users.filter(user => user.id !== userId));
});
};

return (
<div>
<h2>User List</h2>
<UserList users={users} onDeleteUser={handleDeleteUser} />
</div>
);
};

// Presentational Component
const UserList = ({ users, onDeleteUser }) => {
return (
<ul>
{users.map(user => (
<li key={user.id}>
<span>{user.name}</span>
<button onClick={() => onDeleteUser(user.id)}>Delete</button>
</li>
))}
</ul>
);
};

Yukarıdaki örnekte, UserListContainer bileşeni veri işleme ve mantıkla ilgilenirken, UserList bileşeni sadece arayüzü oluşturur. Bu sayede kodun bakımı ve yeniden kullanılabilirliği artar. Container bileşeni, verileri alır, işler ve Presentational bileşenine aktarır. Presentational bileşeni ise aldığı verilere göre arayüzü render eder. Bu şekilde, bileşenlerin sorumlulukları ayrılır ve kodun daha modüler hale gelir.

2- The HOC(Higher-Order Component) Pattern

Bazı bileşenlerde uygulamak istediğimiz bir özelliği tek tek bileşenlere entegre etmek yerine bu özelliği içeren temel daha üst (higher-order) bir bileşen oluşturarak diğer bileşenleri eğer ihtiyaç duyarlarsa parametre olarak bu üst bileşene geçme işlemine bu ad verilir.

Kısaca; bir bileşene ek bir data ya da fonksiyonalite kazandırmak istiyorsak daha üst bileşene parametre olarak geçmemiz yeterli olacaktır.

Bu yapı, React’ın bileşiklendirmeyi miras yapılara tercih etmesinden dolayı (composition over inheritance) kullanım alanı bulmaktadır.

React 16.8 versiyonuyla Hookları hayatımıza sokmazdan önce oldukça yoğun olarak kullanılan bir yapı olarak karşımıza çıkabilmekteydi.

Örnek vermek gerekirse; React-Redux’ın connect() yapısı, React-Router’ın withRouter() yapısı, Material-UI’ın withStyles() yapısı sayılabilir.

Bazı kullanılabilecek alanları sayacak olursak;

  • Bileşenlerin loglanması gerektiğinde,
  • Kimlik doğrulama verisinin gerektiği durumda,
  • 3rd Party bir yapıya ait gerekliliklere ulaşmak isteğimizde,
  • Niş bazı özellikleri kolay biçimde genel bileşenlere geçirmek istediğimizde,
  • Bazı standard layout, scrolling yapılarını sayfalara uygulamak istediğimizde

gibi sıralayarak çoğaltabiliriz.

import React, { useEffect } from 'react';

// HOC oluşturulması
const withAuthentication = (WrappedComponent) => {
// HOC bileşeni
const WithAuthentication = (props) => {
useEffect(() => {
// Kullanıcı oturum durumunu kontrol etmek için burada bir işlem yapılabilir
// Örnek olarak, burada kullanıcı oturum durumu kontrol edilir
// Gerektiğinde yeniden yönlendirme yapılabilir


const isAuthenticated = checkAuthentication();
// Oturum durumunu kontrol et

if (!isAuthenticated) {
// Kullanıcı oturumu yoksa, istediği sayfaya yönlendirme yap
redirectToLoginPage();
}
}, []);

// WrappedComponent'i render et
return <WrappedComponent {...props} />;
};

return WithAuthentication;
};

// HOC ile sarılmış bileşen
const HomePage = () => {
return <div>Welcome to the Home Page!</div>;
};

// HOC ile sarılmış bileşenin oluşturulması
const HomePageWithAuthentication = withAuthentication(HomePage);

// Kullanımı
const App = () => {
return (
<div>
<HomePageWithAuthentication />
</div>
);
};

export default App;

Yukarıdaki örnekte, withAuthentication adında bir HOC oluşturduk. Bu HOC, bir bileşeni saran ve bileşenin mount edilmesi sırasında kullanıcının oturum durumunu kontrol eden bir fonksiyonel bileşen döndürür. HomePage adında bir bileşen oluşturduk ve bu bileşeni withAuthentication HOC'u ile sardık. Sonuç olarak, HomePageWithAuthentication adında bir bileşen elde ettik. Bu bileşeni render ettiğimizde, useEffect hook'u sayesinde bileşen mount edildiğinde kullanıcı oturum durumu kontrol edilir ve gerekirse oturumu olmayan kullanıcılar login sayfasına yönlendirilir. Bu şekilde, HOC patternini kullanarak bileşenlere özellikler eklemek ve kodu yeniden kullanılabilir hale getirmek mümkün olur.

3- Render Props Pattern

React uygulamalarında problemlerin ayrıştırılması(separation of corcern) oldukça önem arz eden bir konudur. Dolayısıyla HOCs patternine benzer yapıda olan render props patterni, bizlere temelde bu konuda yardımcı olmak için ortaya çıkmıştır.

Eğer tekrarlayan algoritmik yapılar ya da fonksiyonlar(logic repetetion) varsa bunları tek bir bileşende kapsülleyip ihtiyaç duyan bileşenlere parametre yolu ile veri aktarımı(sharing data) sağlayabiliriz. Bu kodun tekrar kullanılabilirliğini(code reuse) artıracaktır.

İsimlendirme olarak render tercih edilse de farklı isimlerle çağrılabilir. Hatta children ismi ile çağrılarak React’ın sunduğu isimlendirme avantajından da faydalanılabilir.

Hook yapılarının ortaya çıkması ile büyük oranda kullanımı yoğunluğunu kaybetmiş olsa da çok bilinen Formik, React-Router, React-Datepicker gibi kütüphanelerde kullanımlarına rastlamak mümkündür.

import React, { useState, useEffect } from 'react';

const DataService = {
fetchData: () => {
return new Promise((resolve, reject) => {
// Servis çağrısı yapılıyor (örneğin fetch, axios, vb.)
setTimeout(() => {
// Örnek veri
const data = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' },
];
// Veriyi döndürme
resolve(data);
}, 1000); // 1 saniyelik gecikme ekliyoruz
});
}
};

const DataComponent = ({ render }) => {
const [data, setData] = useState([]);

useEffect(() => {
// Veri çağrısını yap
const fetchData = async () => {
try {
const result = await DataService.fetchData();
setData(result);
} catch (error) {
console.error('Error fetching data:', error);
}
};

fetchData();
}, []); // Sadece bir kez çalışması için boş bağımlılık dizisi

return (
<div>
{render(data)}
</div>
);
};

const App = () => {
return (
<div>
<h2>Data Fetching Example with Render Props</h2>
<DataComponent
render={(data) => (
<ul>
{data.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)}
/>
</div>
);
};

export default App;

Yukarıdaki örnekte, DataService adında bir servis çağrısı yapacak basit bir modül oluşturduk. Bu modül, fetchData adında bir metot içerir ve bu metot asenkron olarak bir veri dizisi döndürür. Daha sonra, DataComponent adında bir bileşen oluşturduk. Bu bileşen, veri çağrısını gerçekleştirir ve dönen veriyi render prop aracılığıyla iletilen bir fonksiyon aracılığıyla dışarıya aktarır. Son olarak, App bileşeninde DataComponent bileşenini kullanarak verileri alıyor ve render ediyoruz. Bu şekilde, veri çağrısı ve render işlemleri bileşenler arasında ayrıştırılmış olur.

Children Prop’u olarak kullanımı:

const DataComponent = ({ children }) => {
const [data, setData] = useState([]);

useEffect(() => {
const fetchData = async () => {
try {
const result = await DataService.fetchData();
setData(result);
} catch (error) {
console.error('Error fetching data:', error);
}
};

fetchData();
}, []);

return (
<div>
{typeof children === 'function' ? children(data) : children}
</div>
);
};

const App = () => {
return (
<div>
<h2>Data Fetching Example with Children Prop</h2>
<DataComponent>
{(data) => (
<ul>
{data.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)}
</DataComponent>
</div>
);
};

export default App;

Yukarıdaki örnekte, DataComponent adında bir bileşen oluşturduk ve bu bileşen children prop'unu kullanarak render edilecek içeriği alır. DataComponent bileşeni, veri çağrısını gerçekleştirir ve dönen veriyi children prop'u aracılığıyla iletilen bir fonksiyon aracılığıyla dışarıya aktarır. Daha sonra, App bileşeninde DataComponent bileşenini kullanarak verileri alıyor ve render ediyoruz.

4-Compound Component Pattern

Birlikte çalışmak amacıyla bir araya getirilmiş bileşenler, bir ana bileşenin altında toplanır. Bu ana bileşen, altındaki çocuk bileşenlere veri ve fonksiyonları geçirir ve bu bileşen hiyerarşisini kendi içinde yönetir. Bu desen, bileşenler arasındaki ilişkiyi yönetmek ve bileşenleri bir arada tutmak için oldukça kullanışlıdır.

Select, Dropdown, Menu, List, Accordion,Tab gibi katmanlı ve hiyerarşik bileşenler bu patternin uygulanması açısından oldukça elverişlidir.

Hook yapıları entegre edilmeden önce içsel veri ve fonksiyon paylaşımı, React’ın sunduğu günümüzde geçerliliği pek kalmamış cloneElement yapısı ile sağlanmaktaydı.

Artık miras(legacy) API’lerden sayılan bu kullanım React dökümanında da öne sürüldüğü haliyle veri paylaşımını üç şekilde yapılmasını tavsiye eder. Render prop ile veri geçme, context yapısı ya da custom Hook kullanımı büyük çoğunlukla bu yapının yerini almış bulunmaktadır.

Şimdi context Hook’u kullanarak bir Accordion bileşeni yapalım.

import React, { createContext, useContext, useState } from 'react';

// AccordionContext oluşturuldu
const AccordionContext = createContext();

// Accordion bileşeni oluşturuldu
const Accordion = ({ children }) => {
const [activeIndex, setActiveIndex] = useState(null);

// Context içerisinde paylaşılacak değerler
const contextValue = {
activeIndex,
setActiveIndex
};

return (
<AccordionContext.Provider value={contextValue}>
<div>{children}</div>
</AccordionContext.Provider>
);
};

// AccordionItem bileşeni oluşturuldu
const AccordionItem = ({ index, children }) => {
const { activeIndex, setActiveIndex } = useContext(AccordionContext);

const handleClick = () => {
setActiveIndex(activeIndex === index ? null : index);
};

const isActive = activeIndex === index;

return (
<div>
<button onClick={handleClick}>
{isActive ? 'Collapse' : 'Expand'}
</button>
{isActive && children}
</div>
);
};

// Accordion.Item bileşenine AccordionItem bileşenini atadık
Accordion.Item = AccordionItem;

// Kullanımı
const App = () => {
return (
<Accordion>
<Accordion.Item index={1}>
<h3>Section 1</h3>
<p>Content for section 1</p>
</Accordion.Item>
<Accordion.Item index={2}>
<h3>Section 2</h3>
<p>Content for section 2</p>
</Accordion.Item>
<Accordion.Item index={3}>
<h3>Section 3</h3>
<p>Content for section 3</p>
</Accordion.Item>
</Accordion>
);
};

export default App;

Yukarıdaki örnekte, Accordion bileşeni, içerisindeki AccordionItem bileşenleri ile birlikte kullanılır. AccordionItem bileşenleri, tıklanıldığında açılıp kapanabilen bölümleri temsil eder. Accordion bileşeni, AccordionContext.Provider’ı kullanarak AccordionItem bileşenlerine veri geçişi sağlar. AccordionItem bileşenleri, useContext Hook’u ile AccordionContext’ten gelen verileri alır ve kullanır. Bu şekilde, veri paylaşımı Accordion bileşeni aracılığıyla gerçekleştirilir.

5-Hooks Pattern

Hook yapıları 2019 yılında hayatımıza girdiğinden beri kendisine geniş bir kullanım alanı yarattı. Fonksiyonel bileşenlere React’ın özellikleri olan context,state, lifecycle ve refs Hook’lar kullanılarak entegre edilmesiyle Class bileşenler popülerliğini yitirmeye başladı.

Class bileşenlerin;

  • ES2015 ile kullanılan class syntax’ının karmaşık yapısı
  • State ve methodların anlaşılması zor mekanizması(bind,constructor,this)
  • Veri geçme işlemlerinin HOCs ve Render Props ile yapılmasındaki yukarıdan aşağı katmanlı hiyerarşinin yönetilmesindeki zorluklar(wrapper hell)
  • Büyüyen bileşen yapılarında artan algoritmik yapıların yönetilme zorluğu ve eklenen lifecycle methodlarının ortaya çıkardığı tekrarlı kod yapıları(componentDidMount , componentDidUpdate , componentWillUnmount)

gibi nedenlerle zaman içinde kullanım zorluğu ortaya çıkarmaktadır. React her ne kadar bu yapıyı desteklemeye devam etse de Hook patterninin Fonksiyonel bileşenler ile kullanımı artık tercih edilen yöntem haline gelmiştir.

Hook’lar sayesinde işlevleri daha küçük, ayrı ve tekrar kullanılabilir parçalara ayırmak mümkündür. Bu sayede React’ın birleşimi miras yapılara tercih(composition over inheritance) etme önceliği de sağlanmış olur.

Ayrıca yukarıdan aşağı derin veri geçme işlemleri yerine yatay mimaride verinin taşınması bileşen yapılarının karmaşısını azaltacaktır.

Bileşenleri esnekliği artarken hem test edilebilirleri hem de okunabilirlikleri artmış olacaktır.

React’ın kendi sağladığı yerleşik(built-in) Hook yapılarını kullanabilir ya da bunları kullanarak kendi kişiselleştirilmiş Hook’larımızı(custom) üretebiliriz.

Hook’lar kullanılırken;

  • React dökümanında bahsedilen kurallara uyulmalıdır ya da bir linter eklentisi ile takip edilmelidir.
  • Hook’ların doğru kullanımı açısından farklı senaryolarda anlaşılması adına önemli bir zaman dilimine ihtiyaç duyabiliriz (useEffect’in dependecy mekanizması vb.).
  • Yanlış ve gereksiz kullanımı uygulamada performans sıkıntıları doğurabilir.(useState re-render mekanizması,useEffect’in dependecy mekanizması vb.).
  • Kullanımı amacını gerçekleştirmeyebilir.(useCallback, useMemo performans optimizasyonu vb.).
  • Custom Hook’ların state ve lifcycle methodlarının taşındığı her bir bileşende tekrar başlatıldığı ve uygulandığının bilincinde olunmalı. Bu durum gereksiz re-render işlemleri ortaya çıkarabilir.
  • Tekrar kullanılabilirliğin yarattığı değişkenliğin yönetilemez hale getirilebileceği unutulmamalıdır. Amacına uygun tek bir işlevi yerine getiren çok parametrik olmayan bir yapı tercih edilmelidir.

SONUÇ:

Sonuç olarak, React.js tasarım desenleri, günümüzün karmaşık ve büyük ölçekli web uygulamalarını geliştirirken karşılaşılan zorlukları aşmak için önemli bir araç seti sunmaktadır. Bu desenler, bileşenler arasındaki iletişimi düzenler, kodun okunabilirliğini arttırır, bakımını kolaylaştırır ve yeniden kullanılabilirliği teşvik eder.

Container/Presentational Pattern, bileşenler arasındaki sorumlulukları net bir şekilde ayırarak kodun daha düzenli tutulmasını sağlar.

HOCs Pattern, bileşenlere ek özellikler kazandırmak için daha genel bir bileşen kullanma yeteneği sağlar ve kodun tekrar kullanılabilirliğini artırır.

Render Props Pattern, veri ve işlevleri paylaşan bileşenler arasında iletişimi sağlar ve bileşenlerin daha modüler olmasını sağlar.

Compound Component Pattern, birlikte çalışan bileşenlerin hiyerarşisini düzenler ve kodun daha düzenli olmasını sağlar.

Hooks Pattern, fonksiyonel bileşenlerle birlikte React’ın özelliklerini kullanarak kodu daha esnek hale getirir, okunabilirliği artırır ve test edilebilirliği sağlar.

Bu tasarım desenlerinin kullanımı, React geliştiricilerinin daha verimli ve etkili bir şekilde uygulama geliştirmesine olanak tanır. Her bir desen, belirli bir sorunu çözmek veya belirli bir ihtiyacı karşılamak için tasarlanmıştır. Dolayısıyla, doğru deseni doğru senaryoya uygulamak, başarılı bir React uygulaması geliştirmenin önemli bir parçasıdır. Tasarım desenlerini anlamak ve kullanmak, React uygulamalarının kalitesini artırabilir ve geliştirme sürecini daha keyifli hale getirebilir.

KAYNAKÇA:

--

--