Menggunakan React.memo untuk mengoptimalkan re-render

1 Agu 2020 · 9 menit baca

Link berhasil di copy

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.

post three image

Kemudian, saat kita menambahkan dua karakter pertama pada salah satu form, log akan bertambah sebagai berikut

post three image

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?

post three image

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

post three image

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).

post three image

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

Emot's Space © 2024