Bu makale patterns.dev adresinde yayınlanan HOC Pattern makalesinin Türkçe çevirisidir.
Yeniden kullanılabilir mantığı, uygulamanızın her yerinde props olarak component'lere aktarın.
Uygulamamız içerisinde, aynı mantığı farklı component'lerde sık sık kullanmak isteriz. Bu mantık, bir component'e belirli bir stil vermek, yetkilendirme sağlamak veya global bir state eklemek olabilir.
Aynı mantığı birden fazla component'te kullanabilmenin bir yolu, Higher Order Component modelini kullanmaktır. Bu model, component mantığını uygulamamızın her yerinde yeniden kullanabilmemizi sağlar.
Higher Order Component (HOC) başka bir component'i parametre olarak alan component'e denir. HOC, parametre olarak aldığı component'e uygulamak istediğimiz mantığı içerir. Bu mantık component'e uygulandıktan sonra; HOC, component'i eklenmiş mantıkla birlikte döndürür.
Diyelim ki, uygulamamızda birden fazla component'e aynı stili uygulamak istiyoruz. Her seferinde local stiller oluşturmak yerine, kendisine aktarılan component'lere belirli bir stil uygulayan bir HOC yaratabiliriz.
function withStyles(Component) {
return props => {
const style = { padding: '0.2rem', margin: '1rem' }
return <Component style={style} {...props} />
}
}
const Button = () = <button>Click me!</button>
const Text = () => <p>Hello World!</p>
const StyledButton = withStyles(Button)
const StyledText = withStyles(Text)
Yukarıda, Button
ve Text
component'lerinin özelleştirilmiş versiyonları olan StyledButton
ve StyledText
component'lerini oluşturduk. Artık iki component de withStyles
HOC'si aracılığıyla eklenmiş stilleri içeriyor.
Şimdi de, daha önce Container/Presentational Pattern içerisinde kullanılmış olan DogImages
örneğini inceleyelim. Bu uygulama, API üzerinden gelen köpek fotoğraflarını listeliyor.
Örneğin kodlarına CodeSandbox üzerinden erişebilirsiniz.
Şimdi kullancı deneyimini biraz geliştirelim. API üzerinden veri gelirken, kullanıcıya bir Loading...
ekranı göstermek istiyoruz. Bu veriyi DogImages
component'ine direkt olarak eklemek yerine, bu mantığı bizim için ekleyecek bir HOC kullanabiliriz.
withLoader
adında bir HOC yaratalım. Bir HOC bir component almalı ve o component'i döndürmelidir. Bu durumda; withLoader
, veri yüklenene kadar Loading...
yazısını gösterecek bir element almalıdır.
Kullanmak istediğimiz withLoader
higher order component'inin yalın bir halini yaratalım.
function withLoader(Element) {
return (props) => <Element />;
}
Ancak, yalnızca alınan elementi döndürmek istemiyoruz. Bunun yerine, alınacak elementin istenen verinin hala yüklenip yüklenmediğini söyleyen kontrolü içermesini istiyoruz.
withLoader
higher order component'ini yeniden kullanılabilir yapabilmek için, Dog API URL'ini hardcoded şekilde yerleştirmeyeceğiz. Bunun yerine, *API URL'*ini withLoader
component'ine argüman olarak geçireceğiz ki bu component, herhangi bir API adresinden veri çekerken Loading...
göstergesine ihtiyaç duyan başka bir component tarafından kullanılabilsin.
function withLoader(Element, url) {
return (props) => {};
}
Higher order component'ler, veri hala yükleniyorken, Loading...
yazısını göstermemizi sağlayan mantığı eklemek istediğimiz, yukarıdaki örnekte props => {}
şeklinde bir functional component olan, bir element döndürür. Veri yüklendiğinde ise, component alınan verileri props olarak higher order component'e geçirmelidir.
Örneğin kodlarına CodeSandbox üzerinden erişebilirsiniz.
Mükemmel! Herhangi bir component ve herhangi bir URL'i alabilen bir HOC yarattık.
useEffect
hook'unda, withLoader
higher order component'i veriyi URL'ini parametre olarak verdiğimiz API adresinden çeker. HOC, veri henüz döndürülmemişken Loading...
yazısını içeren bir element döndürür.
Veri alındığında, bu veriyi data
state'inde tutarız. Artık data
state'i null
olmadığından, HOC'ye verilmiş olan component'i render edebiliriz. Peki, DogImages
listesinde Loading...
göstergesinin görünmesini sağlayacak davranışı uygulamamıza nasıl ekleriz?
Artık DogImages.js
dosyasında, DogImages
component'ini olduğu gibi export
etmek istemiyoruz. Bunun yerine, props yoluyla DogImages
component'ini almış olan withLoading
higher order component'ini export
etmek istiyoruz.
export default withLoading(DogImages);
withLoader
higher order component'i, veriyi hangi adresten çekeceğini bilmek için aynı zamanda URL de bekliyor.
export default withLoader(
DogImages,
"https://dog.ceo/api/breed/labrador/images/random/6"
);
withLoader
component'i döndüreceği elementi, bu durumda DogImages
component'ini, bir data
prop'u ile birlikte döndürdüğünden bu data
prop'una DogImages
component'i içinden erişebiliyoruz.
Örneğin kodlarına CodeSandbox üzerinden erişebilirsiniz.
Mükemmel! Artık veri yüklenirken Loading...
yazısını görebiliyoruz.
Higher Order Component modeli, kullanacağımız mantığı tek yerde tutarken aynı mantığı birden fazla component'e aktarabilmemize olanak sağlıyor. withLoader
component'i kendisine verilen component veya URL ile ilgilenmez. Kısaca, geçerli bir component ve geçerli API adresi olduğu sürece API'dan gelen veriyi component'e aktarır.
Birden fazla higher order component de oluşturabiliriz. Diyelim ki, kullanıcı DogImages
listesinin üzerine geldiğinde Hovering!
yazısını göstermek istiyoruz.
Bunun için, kendisine verdiğimiz elemente bir hovering
prop'u sağlayacak bir HOC yaratmamız gerekiyor. Bu prop'u kullanarak, kullanıcının DogImages
listesinin üzerine gelip gelmediğine bağlı olarak koşullu olarak yazıyı render edebiliriz.
Şimdi de withLoader
higher order component'ini withHover
higher order component'ine aktarabiliriz.
Örneğin kodlarına CodeSandbox üzerinden erişebilirsiniz.
Artık, DogImages
elementi withHover
ve withLoader
tarafından sağlanan tüm prop'ları içeriyor. Şimdi hovering
prop'unun true
ya da false
olması koşuluna bağlı olarak Hovering!
yazısını render edebiliriz.
"recompose, HOC oluşturma konusunda iyi bilinen bir kütüphanedir. Ancak React Hook'ları çoğunlukla HOC'lerin yerini aldığından recompose kütüphanesi artık geliştirilmiyor. Bu sebeple de makale de yer verilmeyecek."
Bazı durumlarda, HOC modeli yerine React Hook'larını kullanabiliriz.
withHover
higher order component'inin yerine useHover
hook'unu kullanalım. Bir HOC oluşturmak yerine, parametre olarak verdiğimiz elemente mouseOver
ve mouseLeave
event listener'larını ekleyen bir hook export ediyoruz. Artık, elementi HOC ile kullandığımız gibi kullanamayız. Onun yerine, mouseOver
ve mouseLeave
event'lerini kontrol etmesi gereken hook'tan bir ref
döndüreceğiz.
Örneğin kodlarına CodeSandbox üzerinden erişebilirsiniz.
useEffect
hook'u component'e bir event listener ekler ve kullanıcının o anda elementi hover edip etmemesine bağlı olarak hovering
state'inin değerini true
ya da false
olarak değiştirir. Hem ref
hem de hovering
değerleri hook'tan döndürülmelidir: ref
değeri mouseOver
ve mouseLeave
event'lerini alacak olan component'e bir referans eklemek için, hovering
ise koşullu olarak Hovering!
yazısını render etmek için döndürülür.
DogImages
component'ini withHover
higher order component'ine vermek yerine, artık useHover
hook'unu direkt olarak DogImages
component'inin içinde kullanabiliriz.
Örneğin kodlarına CodeSandbox üzerinden erişebilirsiniz.
Genel olarak konuşursak, React Hooks HOC modelinin yerini alamaz.
"Çoğu durumda, React Hook'ları yeterli olacaktır ve katmanlı yapıyı azaltmanıza yardımcı olabilir." -React Docs
React'ın dokümantasyonuna göre, hook'ları kullanmak component ağacının derinliğini azaltabilir. HOC modelini kullanırsak da, çok katmanlı bir component ağacıyla karşı karşıya kalmak kolaydır.
<withAuth>
<withLayout>
<withLogging>
<Component />
</withLogging>
</withLayout>
</withAuth>
Component'e direkt olarak bir hook ekleyerek component'leri iç içe kullanmak zorunda kalmayız.
Higher Order Component'leri kullanmak, aynı mantığı tek bir yerde tutup kontrol ederken birden fazla component içinde kullanmayı mümkün kılar. Hook'lar ise component'e özel bir davranış ekleyebilmemizi sağlar, fakat HOC modeliyle karşılaştırıldığında eğer birden fazla component bu davranışa bağlıysa hatayla karşılaşma ihtimalini de artırmış olur.
- Değiştirilmemiş, aynı davranışın uygulama boyunca birdan fazla component tarafından kullanılması gerekiyorsa,
- Component eklenmiş mantık olmadan, tek başına da çalışabiliyorsa,
- Davranışın, onu kullanan her component tarafından özelleştirilmesi gerekiyorsa,
- Davranış uygulamanın her yerinde değil de, bir veya birkaç component tarafından kullanılıyorsa,
- Davranış component'e çok sayıda prop ekliyorsa,
HOC modelini kullanan bazı kütüphaneler hook'ların çıkışından sonra hook desteği eklediler. Bunun güzel bir örneği Apollo Client.
"Bu örneği anlamak için Apollo Client ile ilgili bir tecrübeye ihtiyaç yoktur."
Apollo Client'ı kullanmanın bir yolu graphql()
higher order component'ini kullanmaktır.
Örneğin kodlarına CodeSandbox üzerinden erişebilirsiniz.
graphql()
higher order component'iyle, çekilen veriyi bu higher order component'ine geçirilerek kullanılan component'lerde erişilebilir kılarız. Hala, graphql()
higher order component'ini direkt olarak kullanabiliyor olsak da bazı dezavantajları var.
Bir component birden fazla resolver'e ihtiyaç duyduğunda birden fazla graphql()
higher order component'i compose etmek gerekiyor. Birden fazla HOC compose etmek verinin component'e nasıl aktarıldığının anlamayı zorlaştırıyor. Ayrıca bazı durumlarda HOC'lerin sırasının da önemli olmasıyla birlikte kodu refactor ederken kolayca hataya sebep olabiliyor.
Hook'ların çıkışından sonra; Apollo, Apollo Client kütüphanesine Hooks desteği ekledi. graphql()
higher order component'ini kullanmak yerine, geliştiricler kütüphanenin sağladığı hook'larla artık veriye direkt olarak erişebiliyorlar.
graphql()
higher order component'inin örneğinde gördüğümüz verinin aynısını kullanan bir başka örneği inceleyelim. Bu sefer, component'lere veriyi Apollo Client kütüphanesinin bize sağladığı useMutation
hook'u ile sağlayacağız.
Örneğin kodlarına CodeSandbox üzerinden erişebilirsiniz.
useMutation
hook'unu kullanarak, veriyi sağlamak için yazmamız gereken kod miktarını azaltmış olduk.
Kod miktarındaki azalmanın yanı sıra, bir component'te birden fazla resolver kullanmak da oldukça kolay. Birden fazla HOC compose etmektense, bir component içinde birden fazla hook kullanabiliriz. Verinin component'e nasıl geldiğini anlamak bu şekilde çok daha kolaydır. Ayrıca component'leri refactor ederken ya da daha küçük parçalara ayırırken de geliştirici deneyimini artırmış olur.
HOC modelini kullanmak, yeniden kullanılabilir olmasını istediğimiz mantığın tek bir yerde kalmasını sağlar. bu da aynı kodu uygulama bayınca farklı yerlerde kullanıp hata yayma olasılığımızı azaltır. Tüm mantığı tek bir yerde tutarak, yazdığımız kodu DRY
prensibine uygun yazmış ve seperation of concerns konseptini de zorunlu olarak uygulamış oluruz.
HOC'nin elemente geçirdiği prop'un ismi bir isimlendirme çakışmasına sebep olabilir.
function withStyles(Component) {
return props => {
const style = { padding: '0.2rem', margin: '1rem' }
return <Component style={style} {...props} />
}
}
const Button = () = <button style={{ color: 'red' }}>Click me!</button>
const StyledButton = withStyles(Button)
Bu durumda, withStyles
higher order component'i ona verilen elemana style
adında bir prop geçiriyor. Ama Button
component'inin zaten style
adında bir prop'u var. Prop'ların ismini değiştirerek ya da onları birleştirerek, HOC'nin isim çakışması durumunda düzgün çalışacağından emin olun.
function withStyles(Component) {
return props => {
const style = {
padding: '0.2rem',
margin: '1rem',
...props.style
}
return <Component style={style} {...props} />
}
}
const Button = () = <button style={{ color: 'red' }}>Click me!</button>
const StyledButton = withStyles(Button)
Ona verilen component'e prop'ları aktaran birden fazla compose edilmiş HOC kullanırken, hangi HOC'nin hangi prop'tan sorumlu olduğunu bilmek zor olabilir. Bu da, uygulamanın debug edilmesini ve ölçeklenmesini engelleyebilir.