Komponen dan Hooks harus murni
Fungsi murni hanya melakukan kalkulasi saja. Hal ini membuat kode anda lebih mudah untuk dipahami, di-debug, dan memungkinkan React untuk melakukan optimisasi pada komponen-komponen dan Hooks anda secara otomatis dan benar.
- Mengapa kemurnian itu penting?
- Komponen-komponen dan Hooks harus idempoten
- Efek samping seharusnya berjalan diluar render
- Props dan state adalah tidak dapat dimutasi
- Kembaliakan nilai dan argumen ke Hooks yang tidak dapat dimutasi
- Nilai tidak dapat diubah setelah diteruskan ke JSX
Mengapa kemurnian itu penting?
Salah satu konsep utama dari React adalah kemurnian. Sebuah komponen atau hook disebut murni jika:
- Idempotent - Anda selalu mendapatkan hasil yang sama setiap saat anda menjalankannya dengan masukkan, props, state, context sebagai masukkan komponen;
- Tidak mempunyai efek samping saat render - Kode yang memiliki efek samping seharusnya menjalankannya secara terpisah dari proses me-render. Contohnya adalah sebagai event handler - dimana pengguna berinteraksi dengan UI dan mengakibatkan adanya perubahan; atau sebagai sebuah Efek - yang dijalankan setelah render.
Saat render disimpan murni, React akan memamahi bagaimana caranya untuk memprioritasi proses perubahan mana yang paling penting untuk dilihat oleh pengguna. Hal ini mungkin terjadi karena kemurnian dari render: karena komponen-komponen tidak perlu mempunyai efek samping saat render, React akan menghentikan proses render komponen-komponen yang tidak terlalu penting untuk dilakukan perubahan, dan hanya akan kembali ke komponen tersebut saat diperlukan.
Secara konkrit, hal ini berarti logika me-render akan dijalankan berkali-kali dengan cara yang memungkinan React untuk memberikan pengalaman pengguna (UX) yang menyenangkan kepada pengguannya. Akan tetapi, jika komponen anda memiliki efek samping yang tidak terlacak - seperti mengubah sebuah nilai dari variabel global saat proses render - saat React menjalankan kode proses render anda lagi, efek sampingnya akan dipicu dengan cara yang tidak sesuai dengan yang anda inginkan. Hal ini seringkali menyebabkan bug yang tidak terduga yang dapat menurunkan pengalaman pengguna dalam menggunakan aplikasi anda. Anda dapat melihat contoh dari ini di halaman Menjaga Kemurnian Komponen
Bagaimana React menjalankan kode anda?
React bersifat deklaratif: anda memberi tahu apa kepata React untuk di-render, dan React akan mencari tahu bagaimana cara terbaik untuk menampilkannya kepada pengguna anda. Untuk melakukan ini, React memiliki beberapa fase untuk menjalankan kode anda. Anda tidak perlu untuk mengetahu tentang semua fase yang digunakan React dengan baik. Akan tetapi pada level yang lebih tinggi, anda harus paham tentang kode apa yang dijalankan saat render, dan apa yang berjalan diluar itu.
pe-renderan mengacu pada perhitungan seperti apa tampilan UI anda nantinya. Setelah me-render, Effect di flush (artinya mereka akan dijalankan hingga tidak ada lagi yang tersisa) dan dapat memperbarui kalkulasi jika Effect berdampak pada layout. React akan mengambil kalkulasi ini dan membandingkannya dengan kalkulasi yang digunakannya pada versi sebelumnya dari UI anda, lalu commits hanya perubahan minim yang diperlukan ke DOM (apa yang sebenarnya pengguna lihat) untuk menyesuaikan dengan versi terbaru.
Pendalaman
Salah satu heuristik cepat untuk mengetahui apakah kode berjalan selama render adalah dengan memeriksa di mana kode tersebut berada: jika ditulis di tingkat atas seperti pada contoh di bawah ini, kemungkinan besar kode tersebut berjalan selama render.
function Dropdown() {
const selectedItems = new Set(); // dibentuk saat render
// ...
}
Event handlers and Effects don’t run in render:
function Dropdown() {
const selectedItems = new Set();
const onSelect = (item) => {
// kode ini ada di dalam event handler, jadi hanya dijalankan ketika pengguna memicu ini
selectedItems.add(item);
}
}
function Dropdown() {
const selectedItems = new Set();
useEffect(() => {
// kode ini berada didalam sebuah Effect, sehingga hanya akan jalan saat setelah proses render
logForAnalytics(selectedItems);
}, [selectedItems]);
}
Komponen-komponen dan Hooks harus idempoten
Komponen-komponen harus selalu mengembalikan keluaran yang sama berdasarkan masukan - props, state, dan context. Hal ini dikenal sebagai idempoten. Idempoten adalah istilah yang dipopulerkan pada pemrograman fungsional. Istilah ini mengacu pada gagasan bahwa anda selalu mendapatkan hasil yang sama setiap kali anda menjalankan kode dengan masukan yang sama.
Hal ini berarti semua kode yang dijalankan saat render juga akan bersifat idempoten agar aturan ini dapat diterapkan. Sebagai contoh, barisa kode ini tidak idempoten (dan oleh karena itu, komponennya juga tidak):
function Clock() {
const time = new Date(); // 🔴 Buruk: selalu mengembalikan nilai yang berbeda!
return <span>{time.toLocaleString()}</span>
}
new Date()
tidak idemponten karena selalu mengeluarkan tanggal saat ini dan hasilnya selalu berubah setiap kali dipanggil. Saat anda render komponen di atas, waktu yang ditampilkan pada layar akan tetap pada waktu dimana komponen tersebut di-render. Sama halnya dengan Math.random()
yang juga tidak idempoten, karena selalu mengeluarkan nilai yang berbeda setiap kali dipanggil, walaupun masukan yang diberikan sama.
Hal ini bukan berarti anda tidak seharusnya menggunakan fungsi idemponen seperti new Date()
sama sekali - anda hanya perlu untuk menghindarinya saat render, Dalam kasus ini, kita bisa menyinkronkan tanggal terbaru ke komponen ini dengan Effect:
import { useState, useEffect } from 'react'; function useTime() { // 1. Melacak status tanggal saat ini. `useState` menerima fungsi inisialisasi sebagai // state awal. Fungsi ini hanya berjalan sekali ketika hook dipanggil, jadi hanya tanggal saat ini pada // saat hook dipanggil yang di-set terlebih dahulu. const [time, setTime] = useState(() => new Date()); useEffect(() => { // 2. Perbarui tanggal saat ini setiap detik menggunakan `setInterval`. const id = setInterval(() => { setTime(new Date()); // ✅ Baik: kode non-idempoten tidak lagi berjalan dalam render }, 1000); // 3. Kembalikan fungsi pembersihan agar kita tidak membocorkan timer `setInterval`. return () => clearInterval(id); }, []); return time; } export default function Clock() { const time = useTime(); return <span>{time.toLocaleString()}</span>; }
Dengan membungkus pemanggilan new Date()
yang tidak idempoten di dalam sebuah Effect, ini akan memindahkan kalkulasi tersebut di luar pe-render-an.
Jika Anda tidak perlu menyinkronkan beberapa state eksternal dengan React, Anda juga bisa mempertimbangkan untuk menggunakan event handler jika state tersebut hanya perlu diperbarui sebagai respons terhadap interaksi pengguna.
Efek samping seharusnya berjalan diluar render
Efek samping tidak seharusnya jalan pada render, karena React dapat render komponen-komponen beberapa kali untuk menghasilkan pengalaman pengguna sebaik mungkin.
Meskipun render harus dijaga agar tetap murni, efek samping diperlukan di beberapa titik agar aplikasi Anda dapat melakukan sesuatu yang menarik, seperti menampilkan sesuatu di layar! Poin penting dari aturan ini adalah efek samping tidak boleh dijalankan pada saat render, karena React dapat me-render komponen beberapa kali. Pada kebanyakan kasus, Anda akan menggunakan event handler untuk menangani efek samping. Menggunakan event handler secara eksplisit memberi tahu React bahwa kode ini tidak perlu dijalankan saat render, sehingga render tetap murni. Jika Anda sudah kehabisan semua opsi - dan hanya sebagai pilihan terakhir - Anda juga bisa menangani efek samping menggunakan useEffect
.
Kapan waktu yang tepat untuk melakukan mutasi?
Mutasi local
Satu contoh umu dari efek samping adalah mutasi, yang mana di JavaScript mengacu pada perubahan nilai dari nilai non-primitif. Secara umum, meskipun mutasi tidak bersifat idiomatis di React, mutasi lokal tidak apa-apa:
function FriendList({ friends }) {
const items = []; // ✅ Baik: dibentuk secara lokal
for (let i = 0; i < friends.length; i++) {
const friend = friends[i];
items.push(
<Friend key={friend.id} friend={friend} />
); // ✅ Baik: mutasi lokal tidak apa-apa
}
return <section>{items}</section>;
}
Tidak perlu mengubah kode Anda untuk menghindari mutasi lokal. Array.map
uga dapat digunakan di sini untuk mempersingkat waktu, tetapi tidak ada salahnya membuat mutasi lokal dan kemudian memasukkan item ke dalamnya saat render.
Meskipun terlihat seperti kita melakukan mutasi pada items
, poin penting yang perlu diperhatikan adalah kode ini hanya melakukannya secara lokal - mutasi tidak “diingat” ketika komponen di-render lagi. Dengan kata lain, items
hanya akan tetap ada selama komponen tersebut masih ada. Karena items
selalu dibuat ulang setiap kali <FriendList />
di-render, komponen akan selalu mengembalikan hasil yang sama.
Di sisi lain, jika items
dibuat diluar komponen, maka komponen tersebut akan menyimpan nilai sebelumnya dan mengingat perubahan;
const items = []; // 🔴 Buruk: dibuat di luar komponen
function FriendList({ friends }) {
for (let i = 0; i < friends.length; i++) {
const friend = friends[i];
items.push(
<Friend key={friend.id} friend={friend} />
); // 🔴 Buruk: mengubah nilai yang dibuat di luar render
}
return <section>{items}</section>;
}
Ketika <FriendList />
dijalankan lagi, kita akan terus menambahkan friends
ke items
setiap kali komponen tersebut dijalankan, yang mengarah ke beberapa hasil yang terduplikasi. Versi <FriendList />
ini memiliki efek samping yang dapat diamati selama render dan melanggar aturan.
Lazy initialization
Lazy initialization juga tidak masalah walaupun tidak sepenuhnya “murni”:
function ExpenseForm() {
SuperCalculator.initializeIfNotReady(); // ✅ Baik: jika tidak memengaruhi komponen lain
// Melanjutkan me-render...
}
Mengubah DOM
Efek samping yang secara langsung terlihat oleh pengguna tidak diperbolehkan dalam logika render komponen React. Dengan kata lain, hanya dengan memanggil fungsi komponen seharusnya tidak dengan sendirinya menghasilkan perubahan pada layar.
function ProductDetailPage({ product }) {
document.window.title = product.title; // 🔴 Buruk: Mengubah DOM
}
Salah satu cara untuk mencapai hasil yang diinginkan dengan memperbarui window.title
di luar render adalah dengan menyinkronkan komponen dengan window
.
Selama pemanggilan sebuah komponen beberapa kali aman dan tidak mempengaruhi proses render komponen lainnya, React tidak peduli apakah komponen tersebut 100% murni dalam arti pemrograman fungsional yang ketat. Yang lebih penting adalah komponen harus idempoten.
Props dan state adalah tidak dapat dimutasi
Sebuah props dan state dari komponen adalah snapshots yang tidak dapat dimutasi. Jangan pernah memutasinya secara langsung. Sebagai gantinya, oper props baru kebawah, dan gunakan fungsi setter dari useState
.
Anda dapat menganggap props dan nilai state sebagai snapshot yang diperbarui setelah di-render. Karena alasan ini, Anda tidak memodifikasi props atau variabel state secara langsung: sebagai gantinya, Anda mengoper props baru, atau menggunakan fungsi setter yang disediakan untuk memberi tahu React bahwa state perlu diperbarui pada saat komponen di-render.
Jangan memutasi Props
Props dapat dimutasi karena karena jika anda memutasinya, maka Props tidak dapat diubah karena jika Anda mengubahnya, aplikasi akan menghasilkan output yang tidak konsisten, yang bisa jadi sulit untuk di-debug karena mungkin bekerja atau tidak bekerja tergantung pada situasinya.
function Post({ item }) {
item.url = new Url(item.url, base); // 🔴 Buruk: jangan pernah mengubah props secara langsung
return <Link url={item.url}>{item.title}</Link>;
}
function Post({ item }) {
const url = new Url(item.url, base); // ✅ Baik: buatlah salinan sebagai gantinya
return <Link url={url}>{item.title}</Link>;
}
Jangan memutasi State
useState
mengembalikan variabel state dan sebuah setter untuk mengubah state tersebut.
const [stateVariable, setter] = useState(0);
Daripada memperbarui variabel state di tempat, kita perlu memperbaruinya menggunakan fungsi setter yang dikembalikan oleh useState
. Mengubah nilai pada variabel state tidak menyebabkan komponen diperbarui, sehingga pengguna akan mendapatkan UI yang usang. Menggunakan fungsi setter memberi tahu React bahwa state telah berubah, dan kita perlu mengantri untuk melakukan render ulang untuk memperbarui UI.
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
count = count + 1; // 🔴 Buruk: jangan pernah memutasi state secara langsung
}
return (
<button onClick={handleClick}>
Anda telah menekan {count} kali
</button>
);
}
function Counter() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1); // ✅ Baik: gunakan fungsi setter yang dikeluarkan oleh useState
}
return (
<button onClick={handleClick}>
Anda telah menekan {count} kali
</button>
);
}
Kembaliakan nilai dan argumen ke Hooks yang tidak dapat dimutasi
Sesaat sebuah nilai dioper ke sebuah hook, anda tidak boleh memodifikasinya. Seperti props di JSX, nilai akan berubah menjadi tidak dapat dimutasi saat dioper ke sebuah hook
function useIconStyle(icon) {
const theme = useContext(ThemeContext);
if (icon.enabled) {
icon.className = computeStyle(icon, theme); // 🔴 Buruk: jangan memutasi argumen hook secara langsung
}
return icon;
}
function useIconStyle(icon) {
const theme = useContext(ThemeContext);
const newIcon = { ...icon }; // ✅ Baik: buatlah salinan sebagai gantinya
if (icon.enabled) {
newIcon.className = computeStyle(icon, theme);
}
return newIcon;
}
Salah satu prinsip penting dalam React adalah local reasoning: kemampuan untuk memahami apa yang dilakukan oleh sebuah komponen atau hook dengan melihat kodenya secara terpisah. Hooks harus diperlakukan seperti “kotak hitam” ketika dipanggil. Sebagai contoh, sebuah hook kustom mungkin menggunakan argumennya sebagai dependensi untuk memoisasi nilai di dalamnya:
function useIconStyle(icon) {
const theme = useContext(ThemeContext);
return useMemo(() => {
const newIcon = { ...icon };
if (icon.enabled) {
newIcon.className = computeStyle(icon, theme);
}
return newIcon;
}, [icon, theme]);
}
Jika Anda mengubah argumen Hooks, memoisasi hook kustom akan menjadi salah, jadi penting untuk menghindari hal tersebut.
style = useIconStyle(icon); // `style` dimemoisasi berdasarkan `icon`
icon.enabled = false; // Buruk: 🔴 jangan pernah memutasi argumen hook secara langsung
style = useIconStyle(icon); // sebelumnya mememoisasi hasil yang dikeluarkan
style = useIconStyle(icon); // `style` dimemoisasi berdasarkan `icon`
icon = { ...icon, enabled: false }; // Good: ✅ buatlah salinan sebagai gantinya
style = useIconStyle(icon); // nilai baru dari `style` yang dikalkulasi
Demikian pula, penting untuk tidak memodifikasi nilai yang dikembalikan dari Hooks, karena nilai tersebut mungkin sudah dimemoisasi.
Nilai tidak dapat diubah setelah diteruskan ke JSX
Jangan melakukan mutasi nilai setelah nilai tersebut digunakan dalam JSX. Pindahkan mutasi sebelum JSX dibuat.
Ketika Anda menggunakan JSX dalam sebuah ekspresi, React mungkin akan mengevaluasi JSX sebelum komponen selesai di-render. Ini berarti bahwa mengubah nilai setelah nilai tersebut dioper ke JSX dapat menyebabkan UI yang sudah usang, karena React tidak akan tahu untuk memperbarui keluaran komponen.
function Page({ colour }) {
const styles = { colour, size: "large" };
const header = <Header styles={styles} />;
styles.size = "small"; // 🔴 Buruk: styles telah digunakan sebelumnya di JSX di atas
const footer = <Footer styles={styles} />;
return (
<>
{header}
<Content />
{footer}
</>
);
}
function Page({ colour }) {
const headerStyles = { colour, size: "large" };
const header = <Header styles={headerStyles} />;
const footerStyles = { colour, size: "small" }; // ✅ Baik: kita menggunakan nilai yang baru
const footer = <Footer styles={footerStyles} />;
return (
<>
{header}
<Content />
{footer}
</>
);
}