Pada artikel sebelumnya, saya membahas tentang bagaimanakah proses render terjadi pada React. Kusarankan kamu membaca artikel itu dulu agar lebih mudah memahami artikel kali ini.
Framework React mendapatkan namanya dari sifat alaminya yang bereaksi terhadap perubahan state atau props pada aplikasi. Saat perubahan terjadi, React akan melakukan render ulang pada komponenya lalu mengimplementasikan perubahan tersebut pada DOM.
Namun, terkadang suatu component bisa terkena render ulang (re-render) padahal ia tidak membutuhkanya.
function Title() { console.log("re-rendered"); return <h1>Counter</h1>; } function Counter() { const [count, setCount] = useState(0); const increment = () => setCount((v) => v + 1); return ( <div> <Title /> <p>{count}</p> <button onClick={increment}>Increment</button> </div> ); }
Pada kode diatas saya membuat dua buah komponen React sederhana, yaitu komponen Title
yang berisi teks, dan Counter
yang memiliki fitur menambahkan angka.
Ketika dijalankan, setiap perubahan state pada komponen Counter
akan memicu logging kata "re-rendered"
pada console. Mengapa hal ini terjadi? padahal komponen Title
hanya memiliki teks statis tanpa state maupun props.
Jawabanya sederhana, karena saat terjadi perubahan state atau props pada sebuah komponen, React akan membuat Virtual DOM baru dari komponen yang berubah.
Virtual DOM adalah objek Javascript biasa yang merepresentasikan DOM. React membuat satu Virtual DOM di render awal aplikasi, kemudian terus membuatnya saat terjadi perubahan state atau props.
Setiap Virtual DOM yang dibuat akan dibandingkan dengan yang sebelumnya, untuk melihat adakah perubahan yang akan berpengaruh pada DOM. Proses ini disebut diffing, yang menentukan apa yang harus dirubah pada DOM.
Mari kita bedah kondisi awal Virtual DOM komponen Counter sebelum kita memencet tombol increment pertama kali.
// counter component { type: 'div', props: { children: [ { type: Title, props: null, }, { type: 'p', props: { children: 0, }, }, { type: 'button', props: { onCLick: increment, children: 'Increment', }, }, ], }, };
Setelah tombol increment di pencet, akan terjadi perubahan state dan React membuat Virtual DOM baru yang mencerminkan perubahan tersebut.
// new counter component { type: 'div', props: { children: [ { type: Title, props: null, }, { type: 'p', props: { children: 1, // => change }, }, { type: 'button', props: { onCLick: increment, children: ['Increment'], }, }, ], }, };
Meski komponen Title tidak berubah, <u>namun karena ini adalah objek baru, maka props yang dimiliki pun dianggap baru</u>, dan React perlu merender ulang komponen untuk melakukan cek ulang, apakah dengan props yang baru ini akan merubah bentuk komponen Title (misal, merubah JSX yang ada pada komponen tersebut) ataukah tidak.
Perilaku otomatis dari React ini bisa kita rubah dengan menggunakan React.memo
. Ia bekerja dengan cara 'mengingat' (memoize) hasil render sebuah komponen, dan hanya akan me-render ulang apabila ada perubahan pada props komponen tersebut.
Mari rubah sedikit komponen Title yang kita miliki
const Title = React.memo(function () { console.log("re-rendered"); return <h1>Counter</h1>; });
Setelahnya, pada setiap kamu memencet tombol increment, log berupa kata "re-rendered"
hanya akan muncul satu kali saja, karena tidak ada perubahan props (dan memang komponen Title tidak memiliki props apapun).
Tentu contoh diatas bukanlah best-practice untuk penggunaan React.memo
. Me-render ulang satu komponen sederhana tidak akan memberi dampak apa-apa pada performa aplikasi.
Studi Kasus
Untuk lebih memahami penggunaan React.memo
, saya ingin membuat studi kasus sederhana. Saya akan membuat komponen form yang diiterasi sebanyak n kali, dengan target setiap perubahan state hanya akan merender ulang komponen yang terkait.
Untuk mengawali, kita buat dulu komponen untuk form tersebut dengan 3 field: firstName, lastName, email. Namakan saja IdentityForm.js
import React from "react"; import { useCountRenders } from "./useCountRenders"; export const IdentityForm = ({ data, index, handler }) => { // custom hooks to track counts of re render happened useCountRenders(data, index); return ( <div className="form-group"> <b>{index + 1}. </b> <input type="text" placeholder="First Name" value={data.firstName} onChange={(e) => handler(e.target.value, "firstName", index)} /> <input type="text" placeholder="Last Name" value={data.lastName} onChange={(e) => handler(e.target.value, "lastName", index)} /> <input type="text" placeholder="Email" value={data.email} onChange={(e) => handler(e.target.value, "email", index)} /> </div> ); };
Pada komponen diatas, kita memiliki 3 buah props yaitu: data
berisi value tiap field, index
memberi nomor field, dan akan digunakan sebagai identifier untuk perubahan data, handler
merupakan fungsi untuk mengurus perubahan data.
Selain itu kita juga membuat custom hooks sederhana untuk bantu menghitung berapa kali komponen tersebut di-render ulang, berikut kode untuk useCountRenders.js
import React, { useRef, useEffect } from "react"; export const useCountRenders = (data, index) => { const renders = useRef(0); useEffect(() => console.log( "renders:", renders.current++, "times, for form component number", index + 1 ) ); };
Lalu untuk komponen utama tempat state dan logic berada, buat file dengan nama App.js berisi kode berikut
import React, { useState } from "react"; import { IdentityForm } from "./IdentityForm"; import "./styles.css"; export default function App() { const [states, setStates] = useState( Array(5).fill({ firstName: "", lastName: "", email: "", }) ); const handler = (value = "", key, i) => { setStates((states) => states.map((data, index) => index === i ? { ...data, [key]: value } : data ) ); }; return ( <div className="App"> {states.map((state, index) => ( <IdentityForm key={index} data={state} handler={handler} index={index} /> ))} </div> ); }
Persiapan sudah selesai, tapi nampakanya akan lebih baik kalau saya jelaskan beberapa bagian kode yang sudah kita buat.
// ... const [states, setStates] = useState( Array(5).fill({ firstName: "", lastName: "", email: "", }) ); // ...
Disini kita menggunakan useState
hooks untuk menyimpan data yang kita miliki. Default data nya berupa array berisi 5 buah objek, yang masing-masing objek berisi data untuk tiap komponen IdentityForm.
// ... const handler = (value = "", key, i) => { setStates((states) => states.map((data, index) => index === i ? { ...data, [key]: value } : data ) ); }; // ...
Ini adalah fungsi handler
yang akan kita gunakan untuk mengolah perubahan state pada tiap komponen. Ia bekerja dengan melkakukan map
pada state yang sekarang, dan melakukan perubahan pada data yang sesuai index-nya.
{ states.map((state, index) => ( <IdentityForm key={index} data={state} handler={handler} index={index} /> )); }
Terakhir, pada bagian JSX, kita melakukan map
juga untuk mengiterasi semua data kepada komponen IdentityForm.
Jika kode diatas berhasil dijalankan, kita akan mendapati pemandangan berikut pada browser. Perhatikan pada console, log yang nampak adalah hasil render awal dari masing-masing komponen IdentityForm.
Kemudian, saat kita menambahkan dua karakter pertama pada salah satu form, log akan bertambah sebagai berikut
Dapat dilihat pada console, perubahan pada komponen pertama membuat 4 lainya ikut di render ulang.
Hal tersebut mungkin bukan masalah jika kita hanya punya 5 atau 20 komponen. Namun kamu bisa coba mengganti jumlah array pada states menjadi, misal 5000, dan isi form manapun. Kamu akan mendapati aplikasi menjadi sangat lambat, atau mungkin crash.
Implementasi React.memo
Baik, sekarang kita keluarkan senjata pamungkas React.memo
. Sederhana, rubah sedikit komponen IdentityForm seperti ini
import React from 'react'; import { useCountRenders } from './useCountRenders'; export const IdentityForm = React.memo(({ data, index, handler }) => { // ... content ); });
Lalu kita lihat kembali apakah terjadi perubahan?
What? Kenapa tidak terjadi perubahan? padahal kita jelas sudah menambahkan React.memo
pada komponen IdentityForm.
Callback yang baik
Ingat 3 props yang kita berikan pada komponent IdentityForm, salah satunya adalah handler
, sebuah fungsi untuk mengolah perubahan data pada komponen.
Pada komponen fungsional React, setiap fungsi yang ada dalam komponen akan dibuat ulang (re-created) pada setiap re-render. Jadi, fungsi handler
yang kita jadikan props akan selalu berupa fungsi baru, yang men-trigger render ulang pada komponen IdentityForm, meski menggunakan React.memo
.
Kita bisa menghindari re-creation ini dengan memindahkan fungsi keluar komponen. Namun fungsi handler
kita saat ini menggunakan setStates
untuk merubah data pada useState
hooks, sehingga kita tidak bisa menaruhnya diluar komponen.
Penting, hindari membuat komponen tambahan dalam komponen fungsional React, karena komponen tersebut akan di buat ulang setiap re-render. Buatlah komponen tambahan tersebut diluar komponen utama, atau di file lain.
Untuk mengatasi masalah ini, React memberikan kita satu lagi hooks yang sangat berguna, yaitu useCallback
. Hooks ini bekerja dengan mengingat suatu instance fungsi, dan hanya membuat ulang ketika dependency-nya memiliki perubahan.
Jadi, jika menggunakan React.memo
dengan mengosongkan dependency-nya, kita mendapatkan instance fungsi yang selalu sama. Rubah fungsi handler
pada App.js seperti ini
import React, { useState, useCallback } from "react"; // ... const handler = useCallback((value = "", key, i) => { setStates((states) => states.map((data, index) => index === i ? { ...data, [key]: value } : data ) ); }, []); // ...
Mari kita coba lagi mengisi form yang telah dirubah
Voila! kita berhasil. Render ulang hanya terjadi pada komponen nomor 1 saja. Belum puas? Kita tes lagi dengan menambah jumlah komponen IdentityForm menjadi lebih banyak. Ganti saja angka 5 pada default value di useState
jadi 1000 atau berapapun kamu mau (saranku jangan lebih dari 5000).
Kita berhasil lagi. Hanya komponen nomor 983, yang disitu terjadi perubahan state lah yang di render ulang, Selamat!
Kode utuh dari studi kasus ini dapat kamu lihat disini. Sekian untuk artikel kali ini, jika kamu memiliki feedback atau pertanyaan jangan sungkan untuk mengontakku di Twitter.
Referensi
1. Kent C. Dodds - One Simple trick to optimize React re-renders