- Diterbitkan pada
Salam React, Salam useEffect (InsyaAllah)
- Penulis
- Nama
- Imamuzzaki Abu Salam
- https://x.com/ImBIOS_Dev
Dalam artikel ini, saya akan menunjukkan cara menggunakan React untuk menggantikan useEffect dalam kebanyakan kes.
Saya telah menonton "Goodbye, useEffect" oleh David Khoursid, dan ia 🤯 membuat saya tercengang dalam cara 😀 yang baik. Saya setuju bahawa useEffect telah digunakan begitu banyak sehingga ia menjadikan kod kita kotor dan sukar untuk diselenggara. Saya telah menggunakan useEffect untuk jangka masa yang lama, dan saya bersalah kerana menyalahgunakannya. Saya pasti React mempunyai ciri-ciri yang akan menjadikan kod saya lebih bersih dan mudah diselenggara.
Apakah useEffect?
useEffect adalah cangkuk yang membolehkan kita melakukan kesan sampingan dalam komponen fungsi. Ia menggabungkan componentDidMount, componentDidUpdate, dan componentWillUnmount dalam satu API tunggal. Ia adalah cangkuk yang menarik yang akan membolehkan kita melakukan banyak perkara. Tetapi ia juga merupakan cangkuk yang sangat berbahaya yang boleh menyebabkan banyak pepijat.
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>
}
Ia adalah kaunter mudah yang meningkat setiap saat. Ia menggunakan useEffect untuk menetapkan selang waktu. Ia juga menggunakan useEffect untuk membersihkan selang waktu apabila komponen tidak dipasang. Cuplikan kod di atas adalah kes penggunaan yang meluas untuk useEffect. Ia adalah contoh yang mudah, tetapi ia juga merupakan contoh yang buruk.
Masalah dengan contoh ini ialah selang waktu ditetapkan setiap kali komponen diberikan semula. Jika komponen diberikan semula atas apa pun sebab, selang waktu akan ditetapkan semula. Selang waktu akan dipanggil dua kali setiap saat. Ia bukan masalah dengan contoh mudah ini, tetapi ia boleh menjadi masalah besar apabila selang waktu lebih kompleks. Ia juga boleh menyebabkan kebocoran memori.
Bagaimana untuk memperbaikinya?
Terdapat banyak cara untuk menyelesaikan masalah ini. Salah satu caranya ialah menggunakan useRef untuk menyimpan selang waktu.
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>
}
Kod di atas jauh lebih baik daripada contoh sebelumnya. Ia tidak menetapkan selang waktu setiap kali komponen diberikan semula. Tetapi ia masih memerlukan penambahbaikan. Ia masih agak rumit. Dan ia masih menggunakan useEffect, yang merupakan cangkuk yang sangat berbahaya.
useEffect bukan untuk kesan
Seperti yang kita ketahui tentang useEffect, ia menggabungkan componentDidMount, componentDidUpdate, dan componentWillUnmount dalam satu API tunggal. Mari kita berikan beberapa contohnya:
useEffect(() => {
// componentDidMount?
}, [])
useEffect(() => {
// componentDidUpdate?
}, [something, anotherThing])
useEffect(() => {
return () => {
// componentWillUnmount?
}
}, [])
Ia mudah difahami. useEffect digunakan untuk melakukan kesan sampingan apabila komponen dipasang, dikemas kini, dan tidak dipasang. Tetapi ia bukan sahaja digunakan untuk melakukan kesan sampingan. Ia juga digunakan untuk melakukan kesan sampingan apabila komponen diberikan semula. Ia bukan idea yang baik untuk melakukan kesan sampingan apabila komponen diberikan semula. Ia boleh menyebabkan banyak pepijat. Lebih baik menggunakan cangkuk lain untuk melakukan kesan sampingan apabila komponen diberikan semula.
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>Bilangan perubahan: {count}</div>
</div>
)
}
useEffect bukan pengembang keadaan
import React, { useState, useEffect } from 'react'
const Example = () => {
const [count, setCount] = useState(0)
// Sama seperti componentDidMount dan componentDidUpdate:
useEffect(() => {
// Kemas kini tajuk dokumen menggunakan API pelayar
document.title = `Anda telah klik ${count} kali`
}) // <-- inilah masalahnya, 😱 ia tidak mempunyai array kebergantungan
return (
<div>
<p>Anda telah klik {count} kali</p>
<button onClick={() => setCount(count + 1)}>Klik saya</button>
</div>
)
}
Saya mengesyorkan membaca dokumentasi ini: https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects
Imperatif vs Deklaratif
Imperatif: Apabila sesuatu berlaku, jalankan kesan ini.
Deklaratif: Apabila sesuatu berlaku, ia akan menyebabkan keadaan berubah dan bergantung (array kebergantungan) pada bahagian keadaan yang berubah, kesan ini perlu dijalankan, tetapi hanya jika beberapa syarat benar. Dan React mungkin menjalankannya lagi tanpa sebab pemberian semula serentak.
Konsep vs Pelaksanaan
Konsep:
useEffect(() => {
doSomething()
return () => cleanup()
}, [whenThisChanges])
Pelaksanaan:
useEffect(() => {
if (foo && bar && (baz || quo)) {
doSomething()
} else {
doSomethingElse()
}
// ops, saya terlupa pembersihan
}, [foo, bar, baz, quo])
Pelaksanaan dunia sebenar:
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])
Ia menakutkan untuk menulis kod ini. Lebih-lebih lagi, ia akan menjadi normal dalam pangkalan kod kita dan berantakan. 😱🤮
Di mana kesan berlaku?
React 18 menjalankan kesan dua kali pada pemasangan (dalam mod ketat). Pasang/kesan (╯°□°)╯︵ ┻━┻ -> Lepas pasang (disimulasikan)/pembersihan ┬─┬ /( º _ º /) -> Pasang semula/kesan (╯°□°)╯︵ ┻━┻
Adakah ia perlu diletakkan di luar komponen? useEffect lalai? Uh... janggal. Hmm... 🤔 Kita tidak boleh meletakkannya dalam pemberian semula kerana ia perlu tidak ada kesan sampingan di sana kerana pemberian semula hanya seperti tangan kanan persamaan matematik. Ia perlu menjadi hasil pengiraan sahaja.
Apakah useEffect untuk?
Penyegerakan
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)
}
}, [])
Kesan tindakan vs kesan aktiviti
Tembak-dan-lupakan Disegerakkan
(Kesan tindakan) (Kesan aktiviti)
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 Pasang semula
Di mana kesan tindakan berlaku?
Pengendali peristiwa. Agak.
<form
onSubmit={(event) => {
// 💥 kesan sampingan!
submitData(event)
}}
>
{/* ... */}
</form>
Terdapat maklumat yang sangat baik dalam Beta React.js. Saya mengesyorkan membacanya. Terutama "Bolehkah pengendali peristiwa mempunyai kesan sampingan?" bahagian.
Sudah tentu! Pengendali peristiwa adalah tempat terbaik untuk kesan sampingan.
Satu lagi sumber hebat yang ingin saya sebutkan ialah Di mana anda boleh menyebabkan kesan sampingan
Dalam React, kesan sampingan biasanya berada dalam pengendali peristiwa.
Jika anda telah menghabiskan semua pilihan lain dan tidak dapat mencari pengendali peristiwa yang sesuai untuk kesan sampingan anda, anda masih boleh melampirkannya pada JSX yang anda kembalikan dengan panggilan useEffect dalam komponen anda. Ini memberitahu React untuk menjalankannya kemudian, selepas pemberian semula, apabila kesan sampingan dibenarkan. Walau bagaimanapun, pendekatan ini harus menjadi pilihan terakhir anda.
"Kesan berlaku di luar pemberian semula" - David Khoursid.
(state) => UI
(state, event) => nextState // 🤔 Kesan?
UI adalah fungsi keadaan. Oleh kerana semua keadaan semasa diberikan semula, ia akan menghasilkan UI semasa. Begitu juga, apabila peristiwa berlaku, ia akan membuat keadaan baru. Dan apabila keadaan berubah, ia akan membina UI baru. Paradigma ini adalah teras React.
Bilakah kesan berlaku?
Perisian tengah? 🕵️ Panggilan balik? 🤙 Sagas? 🧙♂️ Reaksi? 🧪 Singki? 🚰 Monad(?) 🧙♂️ Bila-bila masa? 🤷♂️
Peralihan keadaan. Sentiasa.
(state, event) => nextState
|
V
(state, event) => (nextState, effect) // Di sini
![Imej ilustrasi Pemberian semula](https://media.slid.es/uploads/174419/images/9663683/CleanShot_2022-06-22_at_20.24.08_2x.png align="left")
Di mana kesan tindakan berlaku? Pengendali peristiwa. Peralihan keadaan.
Yang kebetulan dijalankan pada masa yang sama dengan pengendali peristiwa.
Kita Mungkin Tidak Memerlukan Kesan
Kita boleh menggunakan useEffect kerana kita tidak tahu bahawa terdapat API yang telah dibina daripada React yang boleh menyelesaikan masalah ini.
Berikut adalah sumber yang sangat baik untuk dibaca mengenai topik ini: Anda Mungkin Tidak Memerlukan Kesan
Kita tidak memerlukan useEffect untuk mengubah data.
useEffect ➡️ useMemo (walaupun kita tidak memerlukan useMemo dalam kebanyakan kes)
const Cart = () => {
const [items, setItems] = useState([])
const [total, setTotal] = useState(0)
useEffect(() => {
setTotal(items.reduce((total, item) => total + item.price, 0))
}, [items])
// ...
}
Baca dan fikirkannya semula dengan teliti 🧐.
const Cart = () => {
const [items, setItems] = useState([])
const total = useMemo(() => {
return items.reduce((total, item) => total + item.price, 0)
}, [items])
// ...
}
Daripada menggunakan useEffect
untuk mengira jumlah keseluruhan, kita boleh menggunakan useMemo
untuk mengingati jumlah keseluruhan. Walaupun pembolehubah bukan pengiraan yang mahal, kita tidak perlu menggunakan useMemo
untuk mengingati ia kerana kita pada dasarnya menukar prestasi untuk memori.
Setiap kali kita melihat setState
dalam useEffect
, ia adalah tanda amaran bahawa kita boleh mempermudahkannya.
useSyncExternalStore
Kesan dengan kedai luaran?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(
// 👇 langgan
storeApi.subscribe,
// 👇 dapatkan snapshot
() => storeApi.getStatus() === 'connected',
// 👇 dapatkan snapshot pelayan
true
)
// ...
}
Kita tidak memerlukan useEffect untuk berkomunikasi dengan ibu bapa.
useEffect ➡️ pengendali peristiwa
❌ 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)
}}
>
Tukar pandangan cepat
</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={}
>
Tukar pandangan cepat
</button>
</div>
)
}
✅ Cara terbaik ialah membuat cangkuk tersuai:
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}
>
Tukar pandangan cepat
</button>
</div>
)
}
Kita tidak memerlukan useEft untuk menginisialisasi singleton global.
useEffect ➡️ justCallIt
❌ Cara yang salah:
const Store = () => {
useEffect(() => {
storeApi.authenticate() // 👈 Ini akan dijalankan dua kali!
}, [])
// ...
}
🔨 Mari 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, huh?
if (typeof window !== 'undefined') {
storeApi.authenticate()
}
const Store = () => {
// ...
}
🧪 Ujian?
const renderApp = () => {
if (typeof window !== 'undefined') {
storeApi.authenticate()
}
appRoot.render(<Store />)
}
Kita tidak semestinya perlu meletakkan semuanya di dalam komponen.
Kita tidak memerlukan useEffect untuk mendapatkan data.
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 Komponen Pelayan cara:
// app/page.tsx
async function getData() {
const res = await fetch('https://api.example.com/...')
// Nilai pulangan *tidak* disirikan
// Anda boleh mengembalikan Date, Map, Set, dll.
// Cadangan: pengendalian ralat
if (!res.ok) {
// Ini akan mengaktifkan `error.js` Error Boundary terdekat
throw new Error('Gagal untuk mendapatkan data')
}
return res.json()
}
export default async function Page() {
const data = await getData()
return <main></main>
}
⏭️💁 Next.js (appDir) dengan useSWR dalam Komponen Klien cara:
// app/page.tsx
import useSWR from 'swr'
export default function Page() {
const { data, error } = useSWR('/api/data', fetcher)
if (error) return <div>gagal untuk memuatkan</div>
if (!data) return <div>memuatkan...</div>
return <div>hello {data}!</div>
}
⏭️🧹 Next.js (pagesDir) dalam SSR cara:
// 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 CSR cara:
// pages/index.tsx
import useSWR from 'swr'
export default function Page() {
const { data, error } = useSWR('/api/data', fetcher)
if (error) return <div>gagal untuk memuatkan</div>
if (!data) return <div>memuatkan...</div>
return <div>hello {data}!</div>
}
🍃 React Query (SSR cara:
import { getItems } from './storeApi'
import { useQuery } from 'react-query'
const Store = () => {
const queryClient = useQueryClient()
return (
<button
onClick={() => {
queryClient.prefetchQuery('items', getItems)
}}
>
Lihat item
</button>
)
}
const Items = () => {
const { data, isLoading, isError } = useQuery('items', getItems)
// ...
}
⁉️ Sungguh ⁉️ Apa yang perlu kita gunakan? useEffect? useQuery? useSWR?
atau... hanya gunakan() 🤔
gunakan() adalah fungsi React baru yang menerima janji secara konseptual serupa dengan await. gunakan() mengendalikan janji yang dikembalikan oleh fungsi dengan cara yang serasi dengan komponen, cangkuk, dan Suspense. Ketahui lebih lanjut mengenai gunakan() dalam React RFC.
function Note({ id }) {
// Ini mendapatkan nota secara tidak segerak, tetapi kepada penulis komponen, ia kelihatan
// seperti operasi segerak.
const note = use(fetchNote(id))
return (
<div>
<h1>{note.title}</h1>
<section>{note.body}</section>
</div>
)
}
Masalah mendapatkan data dalam useEffect
🏃♂️ Keadaan perlumbaan
🔙 Tiada butang belakang segera
🔍 Tiada SSR atau kandungan HTML awal
🌊 Mengejar air terjun
- Reddit, Dan Abramov
Kesimpulan
Daripada mendapatkan data kepada bergelut dengan API imperatif, kesan sampingan adalah salah satu sumber kekecewaan yang paling ketara dalam pembangunan aplikasi web. Dan mari kita jujur, meletakkan semuanya dalam cangkuk useEffect hanya sedikit membantu. Syukurlah, terdapat sains (baiklah, matematik) untuk kesan sampingan, yang diformalkan dalam mesin keadaan dan carta keadaan, yang boleh membantu kita memodelkan dan memahami secara visual bagaimana untuk mengatur kesan, tidak kira betapa kompleksnya mereka secara deklaratif.