- Diterbitkan pada
Halo, React, halo useEffect (semoga begitu)
- Penulis
- Nama
- Imamuzzaki Abu Salam
- https://x.com/ImBIOS_Dev
Mengganti useEffect dengan Hook React Lainnya
Artikel ini akan menunjukkan cara menggunakan React untuk mengganti useEffect
dalam sebagian besar kasus.
Saya telah menonton video "Goodbye, useEffect" oleh David Khoursid, dan itu 🤯 luar biasa dalam cara 😀 yang baik. Saya setuju bahwa useEffect
telah digunakan terlalu banyak sehingga membuat kode kita menjadi kotor dan sulit untuk dipelihara. Saya telah menggunakan useEffect
untuk waktu yang lama, dan saya merasa bersalah telah menyalahgunakannya. Saya yakin React memiliki fitur yang akan membuat kode saya lebih bersih dan mudah dipelihara.
Apa itu useEffect?
useEffect
adalah hook yang memungkinkan kita untuk melakukan efek samping dalam komponen fungsi. Ini menggabungkan componentDidMount
, componentDidUpdate
, dan componentWillUnmount
dalam satu API. Ini adalah hook yang menarik yang akan memungkinkan kita melakukan banyak hal. Tetapi ini juga hook yang sangat berbahaya yang dapat menyebabkan banyak bug.
Mengapa useEffect Berbahaya?
Mari kita lihat contoh berikut:
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>
}
Ini adalah counter sederhana yang meningkat setiap detik. Ini menggunakan useEffect
untuk menetapkan interval. Ini juga menggunakan useEffect
untuk menghapus interval ketika komponen dibongkar. Cuplikan kode di atas adalah kasus penggunaan yang umum untuk useEffect
. Ini adalah contoh yang sederhana, tetapi juga contoh yang buruk.
Masalah dengan contoh ini adalah interval ditetapkan setiap kali komponen dirender ulang. Jika komponen dirender ulang karena alasan apa pun, interval akan ditetapkan lagi. Interval akan dipanggil dua kali per detik. Ini bukan masalah dengan contoh sederhana ini, tetapi dapat menjadi masalah besar ketika intervalnya lebih kompleks. Ini juga dapat menyebabkan kebocoran memori.
Bagaimana Cara Memperbaikinya?
Ada banyak cara untuk memperbaiki masalah ini. Salah satu caranya adalah dengan menggunakan useRef
untuk menyimpan interval.
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>
}
Kode di atas jauh lebih baik daripada contoh sebelumnya. Ini tidak menetapkan interval setiap kali komponen dirender ulang. Tetapi masih perlu ditingkatkan. Itu masih agak rumit. Dan itu masih menggunakan useEffect
, yang merupakan hook yang sangat berbahaya.
useEffect Bukan untuk Efek
Seperti yang kita ketahui tentang useEffect
, ini menggabungkan componentDidMount
, componentDidUpdate
, dan componentWillUnmount
dalam satu API. Mari kita beri beberapa contohnya:
useEffect(() => {
// componentDidMount?
}, [])
useEffect(() => {
// componentDidUpdate?
}, [something, anotherThing])
useEffect(() => {
return () => {
// componentWillUnmount?
}
}, [])
Mudah dimengerti. useEffect
digunakan untuk melakukan efek samping ketika komponen dipasang, diperbarui, dan dibongkar. Tetapi tidak hanya digunakan untuk melakukan efek samping. Ini juga digunakan untuk melakukan efek samping ketika komponen dirender ulang. Bukan ide yang baik untuk melakukan efek samping ketika komponen dirender ulang. Ini dapat menyebabkan banyak bug. Lebih baik menggunakan hook lain untuk melakukan efek samping ketika komponen dirender ulang.
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
bukanlah pengatur state
import React, { useState, useEffect } from 'react'
const Example = () => {
const [count, setCount] = useState(0)
// Mirip dengan componentDidMount dan componentDidUpdate:
useEffect(() => {
// Perbarui judul dokumen menggunakan API browser
document.title = `You clicked ${count} times`
}) // <-- ini adalah masalahnya, 😱 array ketergantungannya hilang
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
)
}
Saya sarankan untuk membaca dokumentasi ini: https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
Imperatif vs Deklaratif
Imperatif: Ketika sesuatu terjadi, jalankan efek ini.
Deklaratif: Ketika sesuatu terjadi, itu akan menyebabkan state berubah dan tergantung (array ketergantungan) pada bagian state mana yang berubah, efek ini harus dijalankan, tetapi hanya jika beberapa kondisi benar. Dan React mungkin menjalankannya lagi tanpa alasan untuk rendering konkuren.
Konsep vs Implementasi
Konsep:
useEffect(() => {
doSomething()
return () => cleanup()
}, [whenThisChanges])
Implementasi:
useEffect(() => {
if (foo && bar && (baz || quo)) {
doSomething()
} else {
doSomethingElse()
}
// oops, saya lupa pembersihan
}, [foo, bar, baz, quo])
Implementasi dunia nyata:
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])
Menakutkan untuk menulis kode ini. Selain itu, itu akan menjadi normal di basis kode kita dan kacau. 😱🤮
Ke Mana Efek Pergi?
React 18 menjalankan efek dua kali saat pasang (dalam mode ketat). Pasang/efek (╯°□°)╯︵ ┻━┻ -> Lepas (simulasi)/bersihkan ┬─┬ /( º _ º /) -> Pasang ulang/efek (╯°□°)╯︵ ┻━┻
Haruskah itu ditempatkan di luar komponen? useEffect
default? Eh... canggung. Hmm... 🤔 Kita tidak bisa memasukkannya ke render karena seharusnya tidak ada efek samping di sana karena render hanya seperti tangan kanan persamaan matematika. Itu hanya harus menjadi hasil dari perhitungan.
Untuk Apa useEffect?
Sinkronisasi
useEffect(() => {
const sub = createThing(input).subscribe((value) => {
// lakukan sesuatu dengan nilai
})
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)
}
}, [])
Efek Aksi vs Efek Aktivitas
Tembak-dan-lupakan Disinkronkan
(Efek Aksi) (Efek Aktivitas)
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-------------------------------------------------------------------------------->
Lepas Pasang Ulang
Ke Mana Efek Aksi Pergi?
Penanganan acara. Sorta.
<form
onSubmit={(event) => {
// 💥 efek samping!
submitData(event)
}}
>
{/* ... */}
</form>
Ada informasi yang sangat baik di Beta React.js. Saya sarankan untuk membacanya. Terutama bagian "Bisakah penanganan acara memiliki efek samping?".
Tentu saja! Penanganan acara adalah tempat terbaik untuk efek samping.
Sumber daya hebat lainnya yang ingin saya sebutkan adalah Di mana Anda dapat menyebabkan efek samping
Dalam React, efek samping biasanya berada di dalam penanganan acara.
Jika Anda telah menghabiskan semua pilihan lain dan tidak dapat menemukan penanganan acara yang tepat untuk efek samping Anda, Anda masih dapat melampirkannya ke JSX yang dikembalikan dengan panggilan
useEffect
di komponen Anda. Ini memberi tahu React untuk menjalankannya nanti, setelah rendering, ketika efek samping diizinkan. Namun, pendekatan ini harus menjadi pilihan terakhir Anda.
"Efek terjadi di luar rendering" - David Khoursid.
(state) => UI
(state, event) => nextState // 🤔 Efek?
UI adalah fungsi dari state. Karena semua state saat ini dirender, itu akan menghasilkan UI saat ini. Demikian pula, ketika suatu acara terjadi, itu akan membuat state baru. Dan ketika state berubah, itu akan membangun UI baru. Paradigma ini adalah inti dari React.
Kapan Efek Terjadi?
Transisi state. Selalu.
(state, event) => nextState
|
V
(state, event) => (nextState, effect) // Di sini
![Ilustrasi gambar rerender](https://media.slid.es/uploads/174419/images/9663683/CleanShot_2022-06-22_at_20.24.08_2x.png align="left")
Ke mana efek aksi pergi? Penanganan acara. Transisi state.
Yang terjadi pada saat yang sama dengan penanganan acara.
Kita Mungkin Tidak Membutuhkan Efek
Kita bisa menggunakan useEffect
karena kita tidak tahu bahwa sudah ada API bawaan dari React yang dapat menyelesaikan masalah ini.
Berikut adalah sumber daya yang sangat baik untuk membaca tentang topik ini: Anda Mungkin Tidak Membutuhkan Efek
useEffect
untuk mentransformasi data.
Kita tidak membutuhkan useEffect
➡️ useMemo
(meskipun kita tidak membutuhkan useMemo
dalam sebagian besar kasus)
const Cart = () => {
const [items, setItems] = useState([])
const [total, setTotal] = useState(0)
useEffect(() => {
setTotal(items.reduce((total, item) => total + item.price, 0))
}, [items])
// ...
}
Baca dan pikirkan lagi dengan cermat 🧐.
const Cart = () => {
const [items, setItems] = useState([])
const total = useMemo(() => {
return items.reduce((total, item) => total + item.price, 0)
}, [items])
// ...
}
Alih-alih menggunakan useEffect
untuk menghitung total, kita dapat menggunakan useMemo
untuk mengingat total. Bahkan jika variabelnya bukan perhitungan yang mahal, kita tidak perlu menggunakan useMemo
untuk mengingatnya karena kita pada dasarnya menukar kinerja dengan memori.
Setiap kali kita melihat setState
di useEffect
, itu adalah tanda peringatan bahwa kita dapat menyederhanakannya.
useSyncExternalStore
Efek dengan toko eksternal?useEffect
➡️ useSyncExternalStore
❌ Cara yang salah:
const Store = () => {
const [isConnected, setIsConnected] = useState(true)
useEffect(() => {
const sub = storeApi.subscribe(({ status }) => {
setIsConnected(status === 'connected')
})
return () => {
sub.unsubscribe()
}
}, [])
// ...
}
✅ Cara terbaik:
const Store = () => {
const isConnected = useSyncExternalStore(
// 👇 berlangganan
storeApi.subscribe,
// 👇 dapatkan snapshot
() => storeApi.getStatus() === 'connected',
// 👇 dapatkan snapshot server
true
)
// ...
}
useEffect
untuk berkomunikasi dengan orang tua.
Kita tidak membutuhkan useEffect
➡️ eventHandler
❌ Cara yang salah:
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>
)
}
📈 Cara yang lebih baik:
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>
)
}
✅ Cara terbaik adalah membuat hook khusus:
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>
)
}
useEft
untuk menginisialisasi singleton global.
Kita tidak membutuhkan useEffect
➡️ justCallIt
❌ Cara yang salah:
const Store = () => {
useEffect(() => {
storeApi.authenticate() // 👈 Ini akan dijalankan dua kali!
}, [])
// ...
}
🔨 Mari kita perbaiki:
const Store = () => {
const didAuthenticateRef = useRef()
useEffect(() => {
if (didAuthenticateRef.current) return
storeApi.authenticate()
didAuthenticateRef.current = true
}, [])
// ...
}
➿ Cara lain:
let didAuthenticate = false
const Store = () => {
useEffect(() => {
if (didAuthenticate) return
storeApi.authenticate()
didAuthenticate = true
}, [])
// ...
}
🤔 Bagaimana jika:
storeApi.authenticate()
const Store = () => {
// ...
}
🍷 SSR, ya?
if (typeof window !== 'undefined') {
storeApi.authenticate()
}
const Store = () => {
// ...
}
🧪 Pengujian?
const renderApp = () => {
if (typeof window !== 'undefined') {
storeApi.authenticate()
}
appRoot.render(<Store />)
}
Kita tidak harus menempatkan semuanya di dalam komponen.
useEffect
untuk mengambil data.
Kita tidak membutuhkan useEffect
➡️ renderAsYouFetch
(SSR) atau useSWR
(CSR)
❌ Cara yang salah:
const Store = () => {
const [items, setItems] = useState([])
useEffect(() => {
let isCanceled = false
getItems().then((data) => {
if (isCanceled) return
setItems(data)
})
return () => {
isCanceled = true
}
})
// ...
}
💽 Cara Remix:
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) dengan async/await
dalam cara Komponen Server:
// app/page.tsx
async function getData() {
const res = await fetch('https://api.example.com/...')
// Nilai pengembalian *bukan* serial
// Anda dapat mengembalikan Date, Map, Set, dll.
// Rekomendasi: tangani kesalahan
if (!res.ok) {
// Ini akan mengaktifkan `error.js` Error Boundary terdekat
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) dengan useSWR
dalam cara Komponen Klien:
// 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) dalam cara SSR:
// 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) dalam cara CSR:
// 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 (cara SSR:
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)
// ...
}
⁉️ Benarkah ⁉️ Apa yang harus kita gunakan? useEffect
?useQuery
? useSWR
?
atau... hanya use()
🤔
use()
adalah fungsi React baru yang menerima promise secara konseptual mirip dengan await
. use()
menangani promise yang dikembalikan oleh fungsi dengan cara yang kompatibel dengan komponen, hook, dan Suspense. Pelajari lebih lanjut tentang use()
di RFC React.
function Note({ id }) {
// Ini mengambil catatan secara asinkron, tetapi bagi penulis komponen, itu terlihat
// seperti operasi sinkron.
const note = use(fetchNote(id))
return (
<div>
<h1>{note.title}</h1>
<section>{note.body}</section>
</div>
)
}
Masalah Mengambil Data di useEffect
🏃♂️ Kondisi balapan
🔙 Tidak ada tombol kembali instan
🔍 Tidak ada SSR atau konten HTML awal
🌊 Mengejar air terjun
- Reddit, Dan Abramov
Kesimpulan
Mulai dari mengambil data hingga berjuang dengan API imperatif, efek samping adalah salah satu sumber frustrasi terbesar dalam pengembangan aplikasi web. Dan mari kita jujur, menempatkan semuanya di hook useEffect
hanya sedikit membantu. Untungnya, ada ilmu (yah, matematika) untuk efek samping, yang diformalkan dalam mesin state dan statechart, yang dapat membantu kita memodelkan dan memahami secara visual cara mengatur efek, tidak peduli seberapa kompleksnya secara deklaratif.