Membedakan Event dengan Effect

Event handler hanya akan dijalankan ulang ketika kita melakukan interaksi yang sama kembali. Berbeda dengan event handler, Effect akan disinkronisasi ulang jika beberapa nilai yang dibaca,seperti prop atau variabel state, berbeda dari apa yang ada pada render sebelumnya. Terkadang Anda juga ingin mencampur perilaku keduanya: Sebuah Effect yang dijalankan ulang menanggapi beberapa nilai tertentu tapi tidak pada yang lainnya. Halaman ini akan mengajari Anda cara melakukannya.

You will learn

  • Cara memilih antara event handler dan Effect
  • Mengapa Effect bersifat reaktif, dan event handler tidak
  • Bagaimana cara kita membuat beberapa bagian dari kode Effect agar tidak reaktif
  • Apa yang dimaksud Effect Event, dan bagaimana untuk mengeluarkan dari Effect Anda
  • Bagaimana cara membaca nilai props dan *state terbaru dari Effects menggunakan Effect Event

Memilih antara event handler atau Effect

Pertama, mari kita rangkum perbedaan antara event handler dan Effects.

Bayangkan Anda sedang menerapkan suatu komponen ruang obrolan (chatroom). Persyaratan Anda terlihat seperti ini:

  1. Komponen Anda harus terhubung secara otomatis ke ruang obrolan yang terpilih.
  2. Ketika Anda menekan tombol ‘Kirim, itu harus mengirimkan pesan ke dalam obrolan.

Katakanlah Anda telah mengimplementasikan kode tersebut, tapi Anda tidak yakin dimana meletakannya. Apakah Anda perlu menggunakan event handler atau Effects? Setiap kali Anda butuh jawaban dari pertanyaan ini, pertimbangkan mengapa kode tersebut perlu dijalankan.

Event handler tereksekusi karena interaksi tertentu

Dari sudut pandang pengguna, pengiriman pesan harus terjadi karena tombol “Kirim” tertentu diklik. Pengguna akan agak kesal jika kita mengirim pesan mereka di waktu lain atau karena alasan lain. Inilah sebabnya mengapa mengirim pesan harus menjadi event handler. Event handler memungkinkan kita menangani interaksi tertentu:

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
// ...
function handleSendClick() {
sendMessage(message);
}
// ...
return (
<>
<input value={message} onChange={e => setMessage(e.target.value)} />
<button onClick={handleSendClick}>Kirim</button>;
</>
);
}

Dengan event handler, kita bisa yakin bahwa sendMessage(message) hanya akan tereksekusi jika pengguna menekan tombol.

Effect tereksekusi ketika sinkronisasi diperlukan

Jangan lupa bahwa kita juga harus menjaga agar komponen kita tetap terhubung dengan ruang obrolan. Kita perlu memikirkan ke mana kode tersebut seharusnya ditempatkan.

Kita harus menjalankan kode tersebut untuk memastikan komponen ini tetap terhubung ke server obrolan yang dipilih, bukan karena interaksi tertentu. Tidak peduli bagaimana atau mengapa pengguna berpindah ke layar ruang obrolan, yang penting adalah sekarang mereka melihatnya dan dapat berinteraksi dengannya. Oleh karena itu, kita perlu memastikan komponen kita tetap terhubung ke server obrolan yang dipilih, bahkan jika pengguna tidak berinteraksi dengan aplikasi kita sama sekali. Inilah sebab mengapa kita perlu menggunakan Effect untuk memastikan hal tersebut terjadi:

function ChatRoom({ roomId }) {
// ...
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
// ...
}

Dengan kode ini, kita dapat memastikan adanya koneksi aktif ke server obrolan yang dipilih saat ini, tanpa perlu bergantung pada interaksi pengguna. Tidak peduli apakah pengguna hanya membuka aplikasi kita, memilih ruangan yang berbeda, atau menavigasi ke layar lain kemudian kembali, Effect dapat memberikan jaminan bahwa komponen akan tetap disinkronisasi dengan ruangan obrolan yang dipilih saat ini. Sehingga, komponen akan selalu terhubung ke server obrolan yang dipilih saat ini dan akan tersambung kembali setiap kali diperlukan.

import { useState, useEffect } from 'react';
import { createConnection, sendMessage } from './chat.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  function handleSendClick() {
    sendMessage(message);
  }

  return (
    <>
      <h1>Selamat datang di room {roomId}!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
      <button onClick={handleSendClick}>Kirim</button>
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [show, setShow] = useState(false);
  return (
    <>
      <label>
        Pilih chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <button onClick={() => setShow(!show)}>
        {show ? 'Tutup chat' : 'Buka chat'}
      </button>
      {show && <hr />}
      {show && <ChatRoom roomId={roomId} />}
    </>
  );
}

Nilai reaktif dan logika reaktif

Secara intuitif, kita bisa mengatakan bahwa event handler selalu dipicu “secara manual”, misalnya dengan mengklik sebuah tombol. Sementara itu, Effect berjalan “secara otomatis”. Mereka berjalan dan berjalan kembali sesering yang diperlukan untuk memastikan sinkronisasi tetap terjaga.

Namun, ada cara yang lebih tepat untuk memikirkan perbedaan antara keduanya.

Props, state, dan variabel yang dideklarasikan di dalam komponen disebut nilai reaktif. Dalam contoh ini, serverUrl bukan merupakan nilai reaktif, melainkan roomId dan message. Keduanya berpartisipasi dalam aliran data rendering, sehingga harus diatur sebagai nilai reaktif agar sinkronisasi dapat terjaga:

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');

// ...
}

Nilai reaktif seperti ini dapat berubah karena rendering ulang suatu komponen. Misalnya, pengguna dapat melakukan beberapa tindakan, seperti mengedit message atau memilih roomId yang berbeda di menu drop-down. Event handler dan Effect merespon perubahan tersebut dengan cara yang berbeda:

  • Kode di dalam event handler bersifat non-reaktif. Ketika sebuah event handler dijalankan (yang disebabkan oleh tindakan pengguna seperti tombol diklik), mereka membaca nilai reaktif tanpa bereaksi terhadap perubahannya. Artinya, jika kita ingin event handler membaca suatu nilai reaktif, mereka tidak akan merespon ketika nilainya berubah kecuali tindakan pengguna yang sama kembali dijalankan.
  • Kode di dalam Effect bersifat reaktif. Jika kita menggunakan Effect untuk membaca nilai reaktif, kita harus mendeklarasikannya sebagai salah satu dependensi Effect tersebut. Kemudian jika render ulang menyebabkan nilai tersebut berubah, React akan menjalankan kembali logika Effect dengan nilai yang baru, sehingga memastikan sinkronisasi data terjaga.

Mari kita lihat kembali contoh sebelumnya untuk mengilustrasikan perbedaan ini.

Kode di dalam event handler bersifat tidak reaktif

Mari kita lihat baris kode ini. Apakah kode ini seharusnya merupakan nilai reaktif atau tidak?

// ...
sendMessage(message);
// ...

Dari sudut pandang pengguna, perubahan dalam nilai message tidak selalu berarti mereka hendak mengirim pesan. Hal ini mungkin hanya berarti bahwa pengguna sedang mengetik. Oleh karena itu, logika pengiriman pesan tidak seharusnya diatur sebagai nilai reaktif, agar tidak dipicu secara otomatis setiap kali nilai message berubah. Sebaliknya, logika ini sebaiknya diimplementasikan pada event handler:

function handleSendClick() {
sendMessage(message);
}

Event handler bersifat tidak reaktif, jadi sendMessage(message) hanya akan tereksekusi saat pengguna mengklik tombol Kirim.

Kode didalam Effect bersifat reaktif

Sekarang mari kita kembali ke baris ini:

// ...
const connection = createConnection(serverUrl, roomId);
connection.connect();
// ...

Dari sudut pandang pengguna, perubahan pada roomId berarti mereka ingin terhubung ke ruangan yang berbeda. Dengan kata lain, logika untuk menghubungkan ke chatroom harus reaktif. Kita ingin baris kode ini “mengikuti” nilai reaktif, dan berjalan lagi jika nilai tersebut berubah. Itu sebabnya kita implementasikan sebagai Effect:

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId]);

Effect bersifat reaktif, jadi createConnection(serverUrl, roomId) dan connection.connect() akan terksekusi setiap kali nilai roomId berubah. Effect ini membantu kita menjaga koneksi tetap tersinkronasi sesuai chatroom yang dipilih saat ini.

Mengekstrak logika non-reaktif dari Effect

Semuanya menjadi lebih kompleks ketika kita ingin menggabungkan logika reaktif dengan logika non-reaktif.

Misalnya, pertimbangkan skenario di mana kita ingin menampilkan notifikasi saat pengguna terhubung ke obrolan. Serta kita juga ingin membaca nilai tema saat ini (terang atau gelap) dari props dari komponen, sehingga notifikasi yang ditampilkan akan memiliki warna yang tepat sesuai dengan tema yang digunakan:

function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Terhubung!', theme);
});
connection.connect();
// ...

Namun, theme adalah nilai reaktif (dapat berubah sebagai hasil dari render ulang), dan setiap nilai reaktif yang dibaca oleh Effect harus dideklarasikan sebagai dependensi Effect tersebut. Sekarang kita harus menentukan theme sebagai dependensi Effect:

function ChatRoom({ roomId, theme }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
showNotification('Terhubung!', theme);
});
connection.connect();
return () => {
connection.disconnect()
};
}, [roomId, theme]); // ✅ Semua dependensi telah didelarasikan
// ...

Cobalah contoh di bawah ini dan cari tahu apakah kamu bisa menemukan masalah pada program berikut:

import { useState, useEffect } from 'react';
import { createConnection, sendMessage } from './chat.js';
import { showNotification } from './notifications.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId, theme }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      showNotification('Terhubung!', theme);
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, theme]);

  return <h1>Selamat datang di room {roomId}!</h1>
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [isDark, setIsDark] = useState(false);
  return (
    <>
      <label>
        Pilih chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Pakai dark theme
      </label>
      <hr />
      <ChatRoom
        roomId={roomId}
        theme={isDark ? 'dark' : 'light'}
      />
    </>
  );
}

Ketika roomId berubah, chat terhubung kembali seperti yang diharapkan. Tetapi karena theme juga termasuk dependensi Effect, chat juga terhubung kembali setiap kita beralih antara tema gelap dan terang. Itu tidak bagus!

Dengan kata lain, kita tidak ingin baris ini menjadi reaktif, meskipun berada di dalam Effect (yang reaktif):

// ...
showNotification('Connected!', theme);
// ...

Kita memerlukan cara untuk memisahkan logika non-reaktif ini dari logika Effect reaktif di sekitarnya.

Mendeklarasikan Effect Event

Under Construction

Bagian ini menjelaskan API eksperimental yang belum dirilis dalam versi React yang stabil.

Gunakan Hook khusus useEffectEvent untuk mengekstrak logika non-reaktif ini dari Effect:

import { useEffect, useEffectEvent } from 'react';

function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Terhubung!', theme);
});
// ...

Disini, onConnected disebut dengan Effect Event. Meskipun merupakan bagian dari logika Effect, namun mempunyai sifat seperti event handler. Logika di dalamnya tidak reaktif, dan selalu memperhatikan nilai terbaru dari props dan state.

Sekarang kita dapat memanggil Effect Event onConnected dari dalam Effect:

function ChatRoom({ roomId, theme }) {
const onConnected = useEffectEvent(() => {
showNotification('Terhubung!', theme);
});

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.on('connected', () => {
onConnected();
});
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Semua dependensi dideklarasikan, karena theme tidak lagi merupakan dependensi Effect, maka tidak akan tereksekusi ulang jika nilai theme berubah.
// ...

Masalah terpecahkan. Perhatikan bahwa kita harus menghapus onConnected dari daftar dependensi Effect. Effect Event tidak reaktif dan harus dihilangkan dari dependensi.

Pastikan bahwa program baru berfungsi seperti yang kita harapkan:

import { useState, useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';
import { createConnection, sendMessage } from './chat.js';
import { showNotification } from './notifications.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification('Terhubung!', theme);
  });

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      onConnected();
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  return <h1>Selamat datang di room {roomId}!</h1>
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [isDark, setIsDark] = useState(false);
  return (
    <>
      <label>
        Pilih chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Gunakan dark theme
      </label>
      <hr />
      <ChatRoom
        roomId={roomId}
        theme={isDark ? 'dark' : 'light'}
      />
    </>
  );
}

Kita dapat menganggap Effect Event sangat mirip dengan event handler. Perbedaan utamanya adalah event handler dijalankan sebagai respons terhadap interaksi pengguna, sedangkan Effect Event dipicu oleh Anda dari Effect. Effect Event memungkinkan kita “memutus rantai” antara reaktivitas Effect dan kode yang seharusnya tidak reaktif.

Membaca props dan state terbaru dengan Effect Event

Under Construction

Bagian ini menjelaskan API eksperimental yang belum dirilis dalam versi React yang stabil.

Effect Event memungkinkan kita memperbaiki banyak pola di mana kamu mungkin tergoda untuk menonaktifkan warning dari dependency linter.

Misalnya, kita memiliki Effect untuk mencatat log kunjungan halaman:

function Page() {
useEffect(() => {
logVisit();
}, []);
// ...
}

Kemudian, kita menambahkan beberapa rute ke situs tersebut. Sekarang komponen Page kita menerima prop url dengan path saat ini. Kita ingin pass url ke parameter function logVisit, tetapi dependency linter pasti akan mengeluarkan warning:

function Page({ url }) {
useEffect(() => {
logVisit(url);
}, []); // 🔴 React Hook useEffect has a missing dependency: 'url'
// ...
}

Sekarang mari pikirkan apa tujuan awal dari kode ini. Kita ingin mencatat setiap kunjungan terpisah untuk tiap URL karena setiap URL merepresentasikan halaman yang berbeda. Artinya, pemanggilan logVisit ini seharusnya berperilaku reaktif terhadap url. Oleh karena itu, dalam kasus ini, akan lebih baik jika kita mengikuti dependency linter dan menambahkan url sebagai salah satu dependensi Effect.

function Page({ url }) {
useEffect(() => {
logVisit(url);
}, [url]); // ✅ Semua dependensi telah dideklarasikan
// ...
}

Sekarang katakanlah kita ingin memasukkan jumlah barang di keranjang belanja bersama dengan setiap kunjungan halaman:

function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;

useEffect(() => {
logVisit(url, numberOfItems);
}, [url]); // 🔴 React Hook useEffect has a missing dependency: 'numberOfItems'
// ...
}

Kita menggunakan numberOfItems di dalam Effect, sehingga linter meminta agar kita menambahkannya sebagai salah satu dependensi. Namun, sebenarnya kita tidak ingin memanggil logVisit secara reaktif terhadap numberOfItems. Jika pengguna menambahkan barang ke dalam keranjang belanja dan numberOfItems berubah, ini tidak berarti bahwa pengguna telah mengunjungi kembali halaman tersebut. Dalam artian lain, melakukan kunjungan ke halaman adalah suatu “peristiwa (event)” yang terjadi pada saat tertentu.

Sebaiknya, pisahkan kode tersebut menjadi dua bagian:

function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;

const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});

useEffect(() => {
onVisit(url);
}, [url]); // ✅ Semua dependensi telah dideklarasikan
// ...
}

Di sini, onVisit adalah suatu Effect Event. Kode di dalamnya tidak bersifat reaktif. Oleh karena itu, kita dapat menggunakan numberOfItems (atau reactive value lain!) tanpa khawatir mengakibatkan kode di sekitarnya dijalankan ulang saat terjadi perubahan.

Namun di sisi lain, Effect itu sendiri tetap bersifat reaktif. Kode di dalam Effect menggunakan prop url, sehingga Effect tersebut akan dijalankan ulang setelah setiap render dengan url yang berbeda. Hal ini pada akhirnya akan memanggil Effect Event onVisit.

Akibatnya, logVisit akan terpanggil untuk setiap perubahan pada url, dan selalu membaca numberOfItems yang terbaru. Namun jika hanya nilai numberOfItems yang berubah, hal ini tidak akan menyebabkan kode berjalan ulang.

Note

Mungkin kamu bertanya-tanya apakah bisa memanggil onVisit() tanpa argumen, dan membaca nilai url di dalamnya:

const onVisit = useEffectEvent(() => {
logVisit(url, numberOfItems);
});

useEffect(() => {
onVisit();
}, [url]);

Cara tersebut memang bisa dilakukan, tetapi sebaiknya kita memasukkan nilai url secara eksplisit ke dalam Effect Event. Dengan memasukkan url sebagai argumen ke dalam Effect Event, kita menyatakan bahwa kunjungan halaman dengan url yang berbeda merupakan sebuah ”event” yang terpisah bagi pengguna. Artinya, visitedUrl merupakan bagian dari ”event” tersebut.

const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});

useEffect(() => {
onVisit(url);
}, [url]);

Karena Effect Event kita secara eksplisit “meminta” nilai visitedUrl, maka sekarang kita tidak dapat secara tidak sengaja menghapus url dari dependensi Effect tersebut. Jika kita menghapus url dari dependensi (dan menyebabkan penghitungan kunjungan halaman yang berbeda terhitung sebagai satu), maka linter akan memberikan peringatan. Kita ingin onVisit berperilaku reaktif terhadap url, sehingga daripada membaca nilai url dari dalam (yang tidak bersifat reaktif), kita pass nilai url dari dalam Effect.

Hal ini menjadi semakin penting jika terdapat beberapa logika asinkron di dalam Effect tersebut:

const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, numberOfItems);
});

useEffect(() => {
setTimeout(() => {
onVisit(url);
}, 5000); // Delay logging visits
}, [url]);

Di sini, nilai url di dalam onVisit merujuk pada nilai url yang terbaru (yang mungkin sudah berubah), sedangkan visitedUrl merujuk pada url yang awalnya menyebabkan Effect ini (dan pemanggilan onVisit ini) dijalankan.

Deep Dive

Apakah boleh untuk tetap menonaktifkan dependency linter?

Di dalam basis kode yang sudah ada, terkadang kamu akan melihat aturan lint dinonaktifkan seperti ini:

function Page({ url }) {
const { items } = useContext(ShoppingCartContext);
const numberOfItems = items.length;

useEffect(() => {
logVisit(url, numberOfItems);
// 🔴 Avoid suppressing the linter like this:
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url]);
// ...
}

Setelah useEffectEvent menjadi bagian versi stabil dari React, kami merekomendasikan untuk tidak menonaktifkan linter.

Kekurangan pertama dari menonaktifkan aturan tersebut adalah bahwa React tidak akan memberikan peringatan lagi ketika Effect yang kita buat perlu “bereaksi” terhadap dependensi reaktif baru yang kita tambahkan ke dalam kode. Pada contoh sebelumnya, kita menambahkan url sebagai dependensi karena React mengingatkannya. Jika kita menonaktifkan linter, secara otomatis tidak akan ada lagi pengingat yang sama untuk perubahan Effect tersebut ke depannya. Hal ini dapat menyebabkan terjadinya bug.

Berikut ini contoh dari bug yang membingungkan yang terjadi karena penonaktifan linter. Pada contoh ini, fungsi handleMove seharusnya membaca nilai variabel state canMove yang terbaru untuk menentukan apakah titik harus mengikuti kursor atau tidak. Namun, canMove selalu bernilai true di dalam handleMove.

Apakah kamu dapat menemukan penyebabnya?

import { useState, useEffect } from 'react';

export default function App() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const [canMove, setCanMove] = useState(true);

  function handleMove(e) {
    if (canMove) {
      setPosition({ x: e.clientX, y: e.clientY });
    }
  }

  useEffect(() => {
    window.addEventListener('pointermove', handleMove);
    return () => window.removeEventListener('pointermove', handleMove);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <label>
        <input type="checkbox"
          checked={canMove}
          onChange={e => setCanMove(e.target.checked)}
        />
        Titik bisa bergerak
      </label>
      <hr />
      <div style={{
        position: 'absolute',
        backgroundColor: 'pink',
        borderRadius: '50%',
        opacity: 0.6,
        transform: `translate(${position.x}px, ${position.y}px)`,
        pointerEvents: 'none',
        left: -20,
        top: -20,
        width: 40,
        height: 40,
      }} />
    </>
  );
}

Masalah pada kode tersebut terletak pada penonaktifan lint dependency. Jika kita hapus penonaktifannya, maka kita akan melihat bahwa Effect tersebut harus membutuhkan fungsi handleMove sebagai dependensi. Hal ini masuk akal, karena handleMove dideklarasikan di dalam badan komponen, yang membuatnya menjadi sebuah nilai reaktif. Setiap nilai reaktif harus dijadikan dependensi, jika tidak, maka nilai tersebut berpotensi menjadi usang dari waktu ke waktu!

Penulis kode tersebut “membohongi” React dengan mengatakan bahwa Effect tersebut tidak memiliki dependensi ([]) pada nilai yang reaktif. Inilah yang menyebabkan React tidak mensinkronisasikan kembali Effect tersebut setelah terjadinya perubahan pada canMove (dan handleMove). Karena React tidak mensinkronisasikan kembali Effect tersebut, maka handleMove yang digunakan sebagai listener adalah fungsi handleMove yang dibuat selama render awal. Selama render awal, canMove bernilai true, itulah sebabnya fungsi handleMove dari render awal akan selalu melihat nilai tersebut.

Dengan tidak pernah menonaktifkan linter dependency, kita tidak akan pernah mengalami masalah dengan nilai yang usang.

Dengan useEffectEvent, tidak perlu “berbohong” pada linter, dan kode dapat bekerja sesuai dengan yang kita harapkan:

import { useState, useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';

export default function App() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const [canMove, setCanMove] = useState(true);

  const onMove = useEffectEvent(e => {
    if (canMove) {
      setPosition({ x: e.clientX, y: e.clientY });
    }
  });

  useEffect(() => {
    window.addEventListener('pointermove', onMove);
    return () => window.removeEventListener('pointermove', onMove);
  }, []);

  return (
    <>
      <label>
        <input type="checkbox"
          checked={canMove}
          onChange={e => setCanMove(e.target.checked)}
        />
        Titik bisa bergerak
      </label>
      <hr />
      <div style={{
        position: 'absolute',
        backgroundColor: 'pink',
        borderRadius: '50%',
        opacity: 0.6,
        transform: `translate(${position.x}px, ${position.y}px)`,
        pointerEvents: 'none',
        left: -20,
        top: -20,
        width: 40,
        height: 40,
      }} />
    </>
  );
}

Hal ini tidak berarti bahwa useEffectEvent selalu menjadi solusi yang tepat. Kita hanya perlu menerapkannya pada baris kode yang tidak ingin bersifat reaktif. Di dalam sandbox di atas, kita tidak ingin kode Effect bersifat reaktif terhadap canMove. Itulah sebabnya masuk akal untuk mengekstrak ke Effect Event.

Baca Menghapus dependensi Effect untuk mengetahui alternatif lain yang tepat selain menonaktifkan linter.

Keterbatasan Effect Event

Under Construction

Bagian ini menjelaskan API eksperimental yang belum dirilis dalam versi React yang stabil.

Effect Event memiliki keterbatasan sebagai berikut:

  • Kita hanya bisa memanggilnya dari dalam Effect.
  • Kita tidak boleh pass Effect Event (sebagai argumen) ke komponen atau Hook lain.

Sebagai contoh, jangan menggunakan Effect Event seperti ini:

function Timer() {
const [count, setCount] = useState(0);

const onTick = useEffectEvent(() => {
setCount(count + 1);
});

useTimer(onTick, 1000); // 🔴 Hindari: Pass Effect Event

return <h1>{count}</h1>
}

function useTimer(callback, delay) {
useEffect(() => {
const id = setInterval(() => {
callback();
}, delay);
return () => {
clearInterval(id);
};
}, [delay, callback]); // Harus mendeklarasikan "callback" pada dependensi
}

Sebaliknya, selalu deklarasikan Effect Event di dekat Effect yang akan menggunakannya:

function Timer() {
const [count, setCount] = useState(0);
useTimer(() => {
setCount(count + 1);
}, 1000);
return <h1>{count}</h1>
}

function useTimer(callback, delay) {
const onTick = useEffectEvent(() => {
callback();
});

useEffect(() => {
const id = setInterval(() => {
onTick(); // ✅ Bagus: Hanya dipanggil di dalam Effect
}, delay);
return () => {
clearInterval(id);
};
}, [delay]); // Tidak perlu mendeklarasikan "onTick" (Effect Event) sebagai dependensi
}

Effect Event merupakan “bagian” yang tidak reaktif dari kode Effect. Effect Event harus dideklarasikan di dekat Effect yang menggunakannya.

Recap

  • Event handler berjalan sebagai respons terhadap interaksi tertentu.
  • Effect berjalan ketika sinkronisasi diperlukan.
  • Logika di dalam event handler tidak bersifat reaktif.
  • Logika di dalam Effect bersifat reaktif.
  • Kita dapat memindahkan logika yang tidak bersifat reaktif dari Effect ke dalam Effect Event.
  • Hanya panggil Effect Event dari dalam Effect.
  • Jangan pass Effect Event (sebagai argumen) ke dalam komponen atau Hook lain.

Challenge 1 of 4:
Memperbaiki variabel yang tidak terupdate

Komponen Timer ini menyimpan variabel state count yang bertambah setiap satu detik. Nilai penambahan disimpan di dalam variabel state increment. Kamu dapat mengontrol variabel increment dengan tombol plus dan minus.

Namun, tidak peduli berapa kali kamu menekan tombol plus, nilai count selalu bertambah satu setiap satu detik. Apa yang salah dengan kode ini? Mengapa increment selalu sama dengan 1 di dalam kode Effect? Cari kesalahan tersebut dan perbaiki.

import { useState, useEffect } from 'react';

export default function Timer() {
  const [count, setCount] = useState(0);
  const [increment, setIncrement] = useState(1);

  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + increment);
    }, 1000);
    return () => {
      clearInterval(id);
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <h1>
        Counter: {count}
        <button onClick={() => setCount(0)}>Reset</button>
      </h1>
      <hr />
      <p>
        Setiap detik, nilai bertambah sebanyak:
        <button disabled={increment === 0} onClick={() => {
          setIncrement(i => i - 1);
        }}></button>
        <b>{increment}</b>
        <button onClick={() => {
          setIncrement(i => i + 1);
        }}>+</button>
      </p>
    </>
  );
}