- Yayınlandı
Merhaba React, Merhaba useEffect (Umarım öyle olur)
- Yazarlar
- Ad
- Imamuzzaki Abu Salam
- https://x.com/ImBIOS_Dev
Bu makalede, çoğu durumda useEffect'i nasıl React ile değiştirebileceğinizi göstereceğim.
David Khoursid'in "Goodbye, useEffect" videosunu izliyorum ve 🤯 aklımı başımdan alıyor 😀 iyi yönde. useEffect'in o kadar çok kullanıldığını ve kodumuzu kirlettiğini ve bakımı zorlaştırdığını kabul ediyorum. Uzun zamandır useEffect kullanıyorum ve kötüye kullanmakla suçluyum. Emin olun, React kodumu daha temiz ve bakımı daha kolay hale getirecek özellikler sunuyor.
useEffect Nedir?
useEffect, fonksiyonel bileşenlerde yan etkiler gerçekleştirmemizi sağlayan bir hook'tur. componentDidMount, componentDidUpdate ve componentWillUnmount'ı tek bir API'de birleştirir. Birçok şey yapabilmemizi sağlayacak güçlü bir hook'tur. Ancak aynı zamanda birçok hata yaratabilecek çok tehlikeli bir hook'tur.
Neden useEffect Tehlikelidir?
Aşağıdaki örneğe bir göz atalım:
import React, { useEffect } from 'react'
const Counter = () => {
const [count, setCount] = useState(0)
useEffect(() => {
const interval = setInterval(() => {
setCount((c) => c + 1)
}, 1000)
return () => clearInterval(interval)
}, [])
return <div>{count}</div>
}
Her saniye artan basit bir sayaçtır. useEffect'i aralık ayarlamak için kullanır. Ayrıca bileşen söküldüğünde aralığı temizlemek için useEffect'i kullanır. Yukarıdaki kod parçası, useEffect için yaygın bir kullanım örneğidir. Basit bir örnektir, ancak aynı zamanda kötü bir örnektir.
Bu örnekteki sorun, aralığın bileşen yeniden işlendiği her seferinde ayarlanmasıdır. Bileşen herhangi bir nedenle yeniden işlenirse, aralık tekrar ayarlanacaktır. Aralık saniyede iki kez çağrılacaktır. Bu basit örnekte bir sorun değil, ancak aralık daha karmaşık olduğunda büyük bir sorun olabilir. Ayrıca bellek sızıntılarına da neden olabilir.
Nasıl düzeltilir?
Bu sorunu düzeltmenin birçok yolu vardır. Bir yol, aralığı depolamak için useRef'i kullanmaktır.
import React, { useEffect, useRef } from 'react'
const Counter = () => {
const [count, setCount] = useState(0)
const intervalRef = useRef()
useEffect(() => {
intervalRef.current = setInterval(() => {
setCount((c) => c + 1)
}, 1000)
return () => clearInterval(intervalRef.current)
}, [])
return <div>{count}</div>
}
Yukarıdaki kod, önceki örnekten çok daha iyidir. Bileşen yeniden işlendiği her seferinde aralığı ayarlamaz. Ancak yine de iyileştirmeye ihtiyaç var. Hala biraz karmaşık. Ve hala çok tehlikeli bir hook olan useEffect'i kullanıyor.
useEffect Etkiler İçin Değildir
Bildiğimiz gibi useEffect, componentDidMount, componentDidUpdate ve componentWillUnmount'ı tek bir API'de birleştirir. Bunun bazı örneklerini verelim:
useEffect(() => {
// componentDidMount?
}, [])
useEffect(() => {
// componentDidUpdate?
}, [something, anotherThing])
useEffect(() => {
return () => {
// componentWillUnmount?
}
}, [])
Anlaması kolay. useEffect, bileşen bağlandığında, güncellendiğinde ve söküldüğünde yan etkiler gerçekleştirmek için kullanılır. Ancak yalnızca yan etkiler gerçekleştirmek için kullanılmaz. Bileşen yeniden işlendiği zaman da yan etkiler gerçekleştirmek için kullanılır. Bileşen yeniden işlendiği zaman yan etkiler gerçekleştirmek iyi bir fikir değildir. Birçok hata yaratabilir. Bileşen yeniden işlendiği zaman yan etkiler gerçekleştirmek için diğer hook'ları kullanmak daha iyidir.
import React, { useState, useEffect } from 'react'
const Example = () => {
const [value, setValue] = useState('')
const [count, setCount] = useState(-1)
useEffect(() => {
setCount(count + 1)
})
const onChange = ({ target }) => setValue(target.value)
return (
<div>
<input type="text" value={value} onChange={onChange} />
<div>Number of changes: {count}</div>
</div>
)
}
useEffect bir state setter'ı değil
import React, { useState, useEffect } from 'react'
const Example = () => {
const [count, setCount] = useState(0)
// componentDidMount ve componentDidUpdate'ye benzer:
useEffect(() => {
// Tarayıcı API'sini kullanarak belge başlığını güncelle
document.title = `You clicked ${count} times`
}) // <-- burası sorun, 😱 bağımlılık dizisi eksik
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
)
}
Bu belgenin okunmasını tavsiye ederim: https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
Emir Verici vs Bildirici
Emir Verici: Bir şey olduğunda, bu etkiyi çalıştır.
Bildirici: Bir şey olduğunda, durumun değişmesine neden olur ve durumun hangi bölümlerinin değiştiğine (bağımlılık dizisi) bağlı olarak, bu etki yürütülmelidir, ancak yalnızca bazı koşullar doğruysa. Ve React nedensiz eşzamanlı işleme için onu tekrar çalıştırabilir.
Konsept vs Uygulama
Konsept:
useEffect(() => {
doSomething()
return () => cleanup()
}, [whenThisChanges])
Uygulama:
useEffect(() => {
if (foo && bar && (baz || quo)) {
doSomething()
} else {
doSomethingElse()
}
// aman, temizlemeyi unuttum
}, [foo, bar, baz, quo])
Gerçek dünya uygulaması:
useEffect(() => {
if (isOpen && component && containerElRef.current) {
if (React.isValidElement(component)) {
ionContext.addOverlay(overlayId, component, containerElRef.current!);
} else {
const element = createElement(component as React.ComponentClass, componentProps);
ionContext.addOverlay(overlayId, element, containerElRef.current!);
}
}
}, [component, containerElRef.current, isOpen, componentProps]);
useEffect(() => {
if (removingValue && !hasValue && cssDisplayFlex) {
setCssDisplayFlex(false)
}
setRemovingValue(false)
}, [removingValue, hasValue, cssDisplayFlex])
Bu kodu yazmak korkutucu. Dahası, kod tabanımızda normal olacak ve berbat olacak. 😱🤮
Etkiler Nereye Gider?
React 18, bağlamada iki kez etkiyi çalıştırır (katı modda). Bağlama/etki (╯°□°)╯︵ ┻━┻ -> Sökme (benzetilmiş)/temizleme ┬─┬ /( º _ º /) -> Yeniden bağlama/etki (╯°□°)╯︵ ┻━┻
Bileşen dışında mı konulmalı? Varsayılan useEffect? Hmm... garip. Hmm... 🤔 Render'a koyamazdık çünkü orada hiçbir yan etki olmamalı çünkü render bir matematik denkleminin sağ tarafı gibidir. Yalnızca hesaplamanın sonucu olmalıdır.
useEffect Ne İçindir?
Senkronizasyon
useEffect(() => {
const sub = createThing(input).subscribe((value) => {
// değere göre bir şey yap
})
return sub.unsubscribe
}, [input])
useEffect(() => {
const handler = (event) => {
setPointer({ x: event.clientX, y: event.clientY })
}
elRef.current.addEventListener('pointermove', handler)
return () => {
elRef.current.removeEventListener('pointermove', handler)
}
}, [])
Eylem Etkileri vs Etkinlik Etkileri
Ateş et ve unut Senkronize
(Eylem etkileri) (Etkinlik etkileri)
0 ---------------------- ----------------- - - -
o o | A | o o | A | A
o o | | | o o | | | |
o o | | | o o | | | |
o o | | | o o | | | |
o o | | | o o | | | |
o o | | | o o | | | |
o o V | V o o V | V |
o-------------------------------------------------------------------------------->
Sökme Yeniden bağlama
Eylem Etkileri Nereye Gider?
Olay işleyicileri. Bir tür.
<form
onSubmit={(event) => {
// 💥 yan etki!
submitData(event)
}}
>
{/* ... */}
</form>
Beta React.js'de mükemmel bilgiler var. Bunu okumanızı tavsiye ederim. Özellikle "Olay işleyicileri yan etkiye sahip olabilir mi?" kısmı.
Elbette! Olay işleyicileri yan etkiler için en iyi yerdir.
Bahsetmek istediğim bir diğer harika kaynak da Yan etki oluşturabileceğiniz yerler
React'te, yan etkiler genellikle olay işleyicileri içinde yer alır.
Tüm diğer seçenekleri denediyseniz ve yan etkiniz için doğru olay işleyicisini bulamadıysanız, yine de bileşeninizde bir useEffect çağrısıyla döndürülen JSX'inize ekleyebilirsiniz. Bu, React'e yan etkilerin izin verildiği yerde, işleme işleminden sonra daha sonra yürütülmesini söyler. Ancak bu yaklaşım son çareniz olmalıdır.
"Etkiler işleme işleminden dışarıda olur" - David Khoursid.
(state) => UI
(state, event) => nextState // 🤔 Etkiler mi?
UI, durumun bir fonksiyonudur. Tüm geçerli durumlar işlendiği için geçerli UI'yi üretir. Benzer şekilde, bir olay meydana geldiğinde, yeni bir durum oluşturur. Ve durum değiştiğinde, yeni bir UI oluşturur. Bu paradigma, React'in çekirdeğidir.
Etkiler Ne Zaman Olur?
Orta katman mı? 🕵️ Geri çağırma işlemleri mi? 🤙 Sagalar mı? 🧙♂️ Tepkiler mi? 🧪 Batıklar mı? 🚰 Monadlar (?) 🧙♂️ Ne zaman? 🤷♂️
Durum geçişleri. Her zaman.
(state, event) => nextState
|
V
(state, event) => (nextState, effect) // Burada
![Yeniden işleme illüstrasyon resmi](https://media.slid.es/uploads/174419/images/9663683/CleanShot_2022-06-22_at_20.24.08_2x.png align="left")
Eylem etkileri nereye gider? Olay işleyicileri. Durum geçişleri.
Bunlar olay işleyicileriyle aynı anda yürütülür.
Etkilere İhtiyacımız Olmayabilir
Bu sorunu çözebilecek yerleşik bir React API'si olduğunu bilmediğimiz için useEffect'i kullanabiliriz.
Bu konu hakkında bilgi edinmek için harika bir kaynak: You Might Not Need an Effect
Verileri dönüştürmek için useEffect'e ihtiyacımız yok.
useEffect ➡️ useMemo (çoğu durumda useMemo'ya ihtiyacımız olmasa da)
const Cart = () => {
const [items, setItems] = useState([])
const [total, setTotal] = useState(0)
useEffect(() => {
setTotal(items.reduce((total, item) => total + item.price, 0))
}, [items])
// ...
}
Tekrar dikkatlice 🧐 okuyun ve düşünün.
const Cart = () => {
const [items, setItems] = useState([])
const total = useMemo(() => {
return items.reduce((total, item) => total + item.price, 0)
}, [items])
// ...
}
Toplamı hesaplamak için useEffect
kullanmak yerine, toplamı belleğe almak için useMemo
'yu kullanabiliriz. Değişken pahalı bir hesaplama değilse bile, useMemo
'yu kullanarak onu belleğe almamıza gerek yok çünkü temelde performansı bellek için değiştiriyoruz.
useEffect
içinde setState
görürsek, bunu basitleştirebileceğimize dair bir uyarı işaretidir.
useSyncExternalStore
Harici depolarla etkiler?useEffect ➡️ useSyncExternalStore
❌ Yanlış yol:
const Store = () => {
const [isConnected, setIsConnected] = useState(true)
useEffect(() => {
const sub = storeApi.subscribe(({ status }) => {
setIsConnected(status === 'connected')
})
return () => {
sub.unsubscribe()
}
}, [])
// ...
}
✅ En iyi yol:
const Store = () => {
const isConnected = useSyncExternalStore(
// 👇 abone ol
storeApi.subscribe,
// 👇 anlık görüntüyü al
() => storeApi.getStatus() === 'connected',
// 👇 sunucu anlık görüntüsünü al
true
)
// ...
}
Ebeveynlerle iletişim kurmak için useEffect'e ihtiyacımız yok.
useEffect ➡️ eventHandler
❌ Yanlış yol:
const ChildProduct = ({ onOpen, onClose }) => {
const [isOpen, setIsOpen] = useState(false)
useEffect(() => {
if (isOpen) {
onOpen()
} else {
onClose()
}
}, [isOpen])
return (
<div>
<button
onClick={() => {
setIsOpen(!isOpen)
}}
>
Toggle quick view
</button>
</div>
)
}
📈 Daha iyi yol:
const ChildProduct = ({ onOpen, onClose }) => {
const [isOpen, setIsOpen] = useState(false)
const handleToggle = () => {
const nextIsOpen = !isOpen;
setIsOpen(nextIsOpen)
if (nextIsOpen) {
onOpen()
} else {
onClose()
}
}
return (
<div>
<button
onClick={}
>
Toggle quick view
</button>
</div>
)
}
✅ En iyi yol, özel bir hook oluşturmaktır:
const useToggle({ onOpen, onClose }) => {
const [isOpen, setIsOpen] = useState(false)
const handleToggle = () => {
const nextIsOpen = !isOpen
setIsOpen(nextIsOpen)
if (nextIsOpen) {
onOpen()
} else {
onClose()
}
}
return [isOpen, handleToggle]
}
const ChildProduct = ({ onOpen, onClose }) => {
const [isOpen, handleToggle] = useToggle({ onOpen, onClose })
return (
<div>
<button
onClick={handleToggle}
>
Toggle quick view
</button>
</div>
)
}
Küresel tekil örnekleri başlatmak için useEft'e ihtiyacımız yok.
useEffect ➡️ justCallIt
❌ Yanlış yol:
const Store = () => {
useEffect(() => {
storeApi.authenticate() // 👈 Bu iki kez çalışacak!
}, [])
// ...
}
🔨 Düzeltme:
const Store = () => {
const didAuthenticateRef = useRef()
useEffect(() => {
if (didAuthenticateRef.current) return
storeApi.authenticate()
didAuthenticateRef.current = true
}, [])
// ...
}
➿ Başka bir yol:
let didAuthenticate = false
const Store = () => {
useEffect(() => {
if (didAuthenticate) return
storeApi.authenticate()
didAuthenticate = true
}, [])
// ...
}
🤔 Ya:
storeApi.authenticate()
const Store = () => {
// ...
}
🍷 SSR, öyle mi?
if (typeof window !== 'undefined') {
storeApi.authenticate()
}
const Store = () => {
// ...
}
🧪 Test mi?
const renderApp = () => {
if (typeof window !== 'undefined') {
storeApi.authenticate()
}
appRoot.render(<Store />)
}
Her şeyi bir bileşen içine yerleştirmemiz gerekmez.
Veri çekmek için useEffect'e ihtiyacımız yok.
useEffect ➡️ renderAsYouFetch (SSR) veya useSWR (CSR)
❌ Yanlış yol:
const Store = () => {
const [items, setItems] = useState([])
useEffect(() => {
let isCanceled = false
getItems().then((data) => {
if (isCanceled) return
setItems(data)
})
return () => {
isCanceled = true
}
})
// ...
}
💽 Remix yolu:
import { useLoaderData } from '@renix-run/react'
import { json } from '@remix-run/node'
import { getItems } from './storeApi'
export const loader = async () => {
const items = await getItems()
return json(items)
}
const Store = () => {
const items = useLoaderData()
// ...
}
export default Store
⏭️🧹 Next.js (appDir) Sunucu Bileşeninde async/await ile:
// app/page.tsx
async function getData() {
const res = await fetch('https://api.example.com/...')
// Dönüş değeri *dizileştirilmemiştir*
// Date, Map, Set vb. döndürebilirsiniz.
// Öneri: hataları işle
if (!res.ok) {
// Bu, en yakın `error.js` Hata Sınırı'nı etkinleştirecektir
throw new Error('Failed to fetch data')
}
return res.json()
}
export default async function Page() {
const data = await getData()
return <main></main>
}
⏭️💁 Next.js (appDir) İstemci Bileşeninde useSWR ile:
// app/page.tsx
import useSWR from 'swr'
export default function Page() {
const { data, error } = useSWR('/api/data', fetcher)
if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>
return <div>hello {data}!</div>
}
⏭️🧹 Next.js (pagesDir) SSR ile:
// pages/index.tsx
import { GetServerSideProps } from 'next'
export const getServerSideProps: GetServerSideProps = async () => {
const res = await fetch('https://api.example.com/...')
const data = await res.json()
return {
props: {
data,
},
}
}
export default function Page({ data }) {
return <div>hello {data}!</div>
}
⏭️💁 Next.js (pagesDir) CSR ile:
// pages/index.tsx
import useSWR from 'swr'
export default function Page() {
const { data, error } = useSWR('/api/data', fetcher)
if (error) return <div>failed to load</div>
if (!data) return <div>loading...</div>
return <div>hello {data}!</div>
}
🍃 React Query (SSR yolu:
import { getItems } from './storeApi'
import { useQuery } from 'react-query'
const Store = () => {
const queryClient = useQueryClient()
return (
<button
onClick={() => {
queryClient.prefetchQuery('items', getItems)
}}
>
See items
</button>
)
}
const Items = () => {
const { data, isLoading, isError } = useQuery('items', getItems)
// ...
}
⁉️ Gerçekten ⁉️ Neyi kullanmalıyız? useEffect? useQuery? useSWR?
veya... sadece use() 🤔
use(), kavramsal olarak await'e benzeyen bir sözü kabul eden yeni bir React fonksiyonudur. use(), bir fonksiyon tarafından döndürülen sözü, bileşenlerle, hook'larla ve Suspense ile uyumlu bir şekilde işler. use() hakkında React RFC'de daha fazla bilgi edinin.
function Note({ id }) {
// Bu, bir notu eşzamansız olarak alır, ancak bileşen yazarına,
// eşzamansız bir işlem gibi görünür.
const note = use(fetchNote(id))
return (
<div>
<h1>{note.title}</h1>
<section>{note.body}</section>
</div>
)
}
useEffect'te Veri Çekmede Sorunlar
🏃♂️ Yarış koşulları
🔙 Anında geri düğmesi yok
🔍 SSR veya başlangıç HTML içeriği yok
🌊 Şelale kovalamaca
- Reddit, Dan Abramov
Sonuç
Veri çekmekten, emir verici API'lerle mücadele etmeye kadar, yan etkiler web uygulaması geliştirmede en büyük hayal kırıklığı kaynaklarından biridir. Ve dürüst olmak gerekirse, her şeyi useEffect hook'larına koymak yalnızca biraz yardımcı olur. Neyse ki, yan etkiler için, ne kadar karmaşık olurlarsa olsunlar, etkileri nasıl bildirici olarak düzenleyeceğimizi görselleştirmemize ve anlamamıza yardımcı olabilecek, durum makineleri ve durum şemalarında resmileştirilmiş bir bilim (yani matematik) var.