Menggunakan ulang logika dengan Custom Hooks

React dilengkapi dengan beberapa Hook bawaan seperti useState, useContext, dan useEffect. Terkadang, Anda mungkin ingin ada Hook untuk tujuan yang lebih spesifik: misalnya, untuk mengambil data, melacak apakah pengguna sedang online, atau terhubung ke ruang obrolan. Anda mungkin tidak menemukan Hook ini di React, tetapi Anda dapat membuat Hook Anda sendiri untuk kebutuhan aplikasi Anda.

Anda akan mempelajari

  • Apa itu custom Hooks, dan bagaimana menulis Hooks sendiri
  • Bagaimana cara menggunakan ulang logika antara komponen
  • Bagaimana memberi nama dan mengatur struktur Hooks yang dibuat sendiri
  • Kapan dan mengapa harus mengekstrak custom Hooks

Custom Hooks: Berbagi logika antar komponen

Bayangkan Anda sedang mengembangkan aplikasi yang sangat bergantung pada jaringan (seperti kebanyakan aplikasi). Anda ingin memberi peringatan kepada pengguna jika koneksi jaringannya tiba-tiba terputus saat mereka menggunakan aplikasi Anda. Bagaimana cara melakukannya? Sepertinya Anda akan memerlukan dua hal dalam komponen Anda:

  1. Sebuah keadaan (state) yang melacak apakah jaringan online.
  2. Sebuah Efek yang berlangganan (subscribes) pada peristiwa (events) online dan offline global, dan memperbarui state tersebut.

Hal ini akan menjaga sinkronisasi komponen Anda dengan status jaringan. Anda mungkin memulainya dengan sesuatu seperti ini:

import { useState, useEffect } from 'react';

export default function StatusBar() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

Coba matikan dan nyalakan jaringan Anda, dan perhatikan bagaimana StatusBar ini diperbarui sebagai respons terhadap tindakan Anda.

Sekarang bayangkan Anda juga ingin menggunakan logika yang sama pada komponen yang berbeda. Anda ingin mengimplementasikan tombol Simpan yang akan menjadi tidak aktif dan menunjukkan “Sedang menghubungkan kembali…” alih-alih “Simpan” saat jaringan mati.

Untuk memulai, Anda dapat menyalin dan mem-paste state isOnline dan Efeknya ke dalam SaveButton:

import { useState, useEffect } from 'react';

export default function SaveButton() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  function handleSaveClick() {
    console.log('✅ Progress saved');
  }

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? 'Save progress' : 'Reconnecting...'}
    </button>
  );
}

Pastikan, jika Anda mematikan jaringan, tampilan tombol tersebut akan berubah.

Kedua komponen ini berfungsi dengan baik, tetapi duplikasi logika di antara keduanya tidak diinginkan. Meskipun mereka memiliki tampilan visual yang berbeda, Anda ingin menggunakan kembali logika yang sama di antara keduanya.

Mengekstrak custom Hook Anda sendiri dari sebuah komponen

Bayangkan bahwa, serupa dengan useState dan useEffect, ada sebuah Hook useOnlineStatus bawaan. Kemudian kedua komponen ini bisa disederhanakan dan Anda dapat menghapus duplikasi di antara keduanya:

function StatusBar() {
const isOnline = useOnlineStatus();
return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

function SaveButton() {
const isOnline = useOnlineStatus();

function handleSaveClick() {
console.log('✅ Progress saved');
}

return (
<button disabled={!isOnline} onClick={handleSaveClick}>
{isOnline ? 'Save progress' : 'Reconnecting...'}
</button>
);
}

Meskipun tidak ada Hook bawaan seperti itu, Anda dapat menulisnya sendiri. Deklarasikan fungsi bernama useOnlineStatus dan pindahkan semua kode duplikat ke dalamnya dari komponen yang Anda tulis sebelumnya:

function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
function handleOnline() {
setIsOnline(true);
}
function handleOffline() {
setIsOnline(false);
}
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}

Di akhir fungsi, kembalikan isOnline. Ini memungkinkan komponen Anda membaca nilai itu:

import { useOnlineStatus } from './useOnlineStatus.js';

function StatusBar() {
  const isOnline = useOnlineStatus();
  return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}

function SaveButton() {
  const isOnline = useOnlineStatus();

  function handleSaveClick() {
    console.log('✅ Progress saved');
  }

  return (
    <button disabled={!isOnline} onClick={handleSaveClick}>
      {isOnline ? 'Save progress' : 'Reconnecting...'}
    </button>
  );
}

export default function App() {
  return (
    <>
      <SaveButton />
      <StatusBar />
    </>
  );
}

Pastikan bahwa mengubah jaringan on dan off memperbarui kedua komponen.

Sekarang komponen Anda tidak memiliki logika berulang. Lebih penting lagi, kode di dalamnya menjelaskan apa yang ingin mereka lakukan (gunakan status online!) daripada bagaimana melakukannya (dengan berlangganan events browser).

Ketika Anda mengekstrak logika ke dalam custom Hooks, Anda dapat menyembunyikan detail-detail rumit tentang bagaimana Anda menangani sistem eksternal atau API browser. Kode komponen Anda mengekspresikan niat Anda, bukan implementasi.

Nama hook selalu diawali dengan use

Aplikasi React dibangun dari komponen. Komponen dibuat dari Hooks, baik bawaan maupun kustom. Anda mungkin akan sering menggunakan Hooks khusus yang dibuat oleh orang lain, tetapi kadang-kadang Anda mungkin menulisnya sendiri!

Anda harus mengikuti konvensi penamaan ini:

  1. Nama komponen React harus dimulai dengan huruf kapital, seperti StatusBar dan SaveButton. Komponen React juga perlu mengembalikan sesuatu yang dapat ditampilkan oleh React, seperti sebuah potongan JSX.
  2. Nama hook harus dimulai dengan use diikuti dengan huruf kapital, seperti useState (bawaan) atau useOnlineStatus (kustom, seperti yang ditunjukkan pada contoh sebelumnya). Hooks dapat mengembalikan nilai arbitrer.

Konvensi ini menjamin bahwa Anda selalu dapat melihat sebuah komponen dan mengetahui di mana letak state, Efek, dan fitur React lainnya mungkin “bersembunyi”. Misalnya, jika Anda melihat sebuah panggilan fungsi getColor() di dalam komponen Anda, Anda bisa yakin bahwa panggilan itu tidak mungkin mengandung state React di dalamnya karena namanya tidak dimulai dengan use. Namun, sebuah panggilan fungsi seperti useOnlineStatus() kemungkinan besar akan mengandung panggilan Hook lain di dalamnya!

Catatan

Jika linter Anda dikonfigurasi untuk React, maka linter akan memberlakukan konvensi penamaan ini. Gulir ke atas ke sandbox di atas dan ganti nama useOnlineStatus menjadi getOnlineStatus. Perhatikan bahwa linter tidak mengizinkan Anda memanggil useState atau useEffect di dalamnya lagi. Hanya Hook dan komponen saja yang dapat memanggil Hook lainnya!

Pendalaman

Apakah semua fungsi yang dipanggil selama rendering harus diawali dengan awalan use?

Tidak. Fungsi yang tidak memanggil Hooks tidak perlu menjadi Hooks.

Jika fungsi Anda tidak memanggil Hooks apa pun, hindari awalan use. Sebagai gantinya, tulislah sebagai fungsi biasa tanpa awalan use. Misalnya, useSorted di bawah ini tidak memanggil Hooks, jadi panggil saja getSorted:

// 🔴 Hindari: Sebuah Hook yang tidak menggunakan Hooks
function useSorted(items) {
return items.slice().sort();
}

// ✅ Baik: Sebuah fungsi biasa yang tidak menggunakan Hooks
function getSorted(items) {
return items.slice().sort();
}

Ini memastikan bahwa kode Anda dapat memanggil fungsi biasa ini di mana saja, termasuk pada sebuah kondisi:

function List({ items, shouldSort }) {
let displayedItems = items;
if (shouldSort) {
// ✅ Tidak apa-apa untuk memanggil getSorted() secara kondisional karena ini bukan sebuah Hook
displayedItems = getSorted(items);
}
// ...
}

Anda harus memberikan awalan use ke sebuah fungsi (dan dengan demikian menjadikannya sebuah Hook) jika fungsi tersebut menggunakan setidaknya satu Hook di dalamnya:

// ✅ Baik: Sebuah Hook yang menggunakan Hook lainnya
function useAuth() {
return useContext(Auth);
}

Secara teknis, ini tidak diberlakukan oleh React. Pada prinsipnya, Anda dapat membuat sebuah Hook yang tidak memanggil Hook lain. Ini seringkali membingungkan dan membatasi jadi sebaiknya hindari pola itu. Namun, mungkin ada kasus yang jarang terjadi di mana itu sangat membantu. Misalnya, mungkin fungsi Anda tidak menggunakan Hooks saat ini, tetapi Anda berencana untuk menambahkan beberapa panggilan Hooks ke dalamnya di masa mendatang. Maka masuk akal untuk menamainya dengan awalan use:

// ✅ Baik: Sebuah Hook yang kemungkinan akan menggunakan beberapa Hook lainnya nanti
function useAuth() {
// TODO: Ganti dengan baris ini saat autentikasi diterapkan:
// return useContext(Auth);
return TEST_USER;
}

Maka komponen tidak akan dapat memanggilnya secara kondisional. Ini akan menjadi penting ketika Anda benar-benar menambahkan panggilan Hook di dalamnya. Jika Anda tidak berencana untuk menggunakan Hooks di dalamnya (sekarang atau nanti), jangan menjadikannya sebagai Hook.

Custom Hooks memungkinkan Anda berbagi logika yang berkelanjutan, bukan state itu sendiri

Pada contoh sebelumnya, ketika Anda menghidupkan dan mematikan jaringan, kedua komponen diperbarui secara bersamaan. Namun, salah jika berpikir bahwa satu variabel state isOnline dibagikan di antara keduanya. Lihat kode ini:

function StatusBar() {
const isOnline = useOnlineStatus();
// ...
}

function SaveButton() {
const isOnline = useOnlineStatus();
// ...
}

Ini bekerja dengan cara yang sama seperti sebelumnya sebelum duplikasi diekstrak:

function StatusBar() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
// ...
}, []);
// ...
}

function SaveButton() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
// ...
}, []);
// ...
}

Ini adalah dua variabel state dan Efek yang sepenuhnya independen! Mereka kebetulan memiliki nilai yang sama pada saat yang sama karena Anda menyinkronkannya dengan nilai eksternal yang sama (apakah jaringan sedang hidup).

Untuk menggambarkan hal ini dengan lebih baik, kita memerlukan contoh yang berbeda. Pertimbangkan komponen Form ini:

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('Mary');
  const [lastName, setLastName] = useState('Poppins');

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
  }

  return (
    <>
      <label>
        First name:
        <input value={firstName} onChange={handleFirstNameChange} />
      </label>
      <label>
        Last name:
        <input value={lastName} onChange={handleLastNameChange} />
      </label>
      <p><b>Good morning, {firstName} {lastName}.</b></p>
    </>
  );
}

Ada beberapa logika yang berulang untuk setiap kolom formulir:

  1. Ada sebuah state (firstName dan lastName).
  2. Ada handler perubahan (handleFirstNameChange dan handleLastNameChange).
  3. Ada JSX yang menentukan atribut value dan onChange untuk input tersebut.

Anda dapat mengekstrak logika yang berulang ke dalam Custom Hook useFormInput ini:

import { useState } from 'react';

export function useFormInput(initialValue) {
  const [value, setValue] = useState(initialValue);

  function handleChange(e) {
    setValue(e.target.value);
  }

  const inputProps = {
    value: value,
    onChange: handleChange
  };

  return inputProps;
}

Perhatikan bahwa itu hanya mendeklarasikan satu variabel state yang disebut value.

Namun, komponen Form memanggil useFormInput dua kali:

function Form() {
const firstNameProps = useFormInput('Mary');
const lastNameProps = useFormInput('Poppins');
// ...

Inilah sebabnya mengapa itu bekerja seperti mendeklarasikan dua variabel state yang terpisah!

Custom Hooks memungkinkan Anda berbagi logika berkelanjutan tetapi tidak state itu sendiri. Setiap panggilan Hook sepenuhnya independen dari setiap panggilan ke Hook yang sama. Inilah mengapa kedua sandbox di atas sepenuhnya setara. Jika Anda mau, gulir ke atas dan bandingkan. Perilaku sebelum dan sesudah mengekstrak Custom Hook itu identik.

Ketika Anda perlu membagikan state itu sendiri antara beberapa komponen, angkat dan lewatkan ke bawah sebagai gantinya.

Mengirimkan nilai reaktif antara Hooks

Kode di dalam kustom Hooks akan dijalankan kembali setiap kali komponen Anda di-render ulang. Oleh karena itu, seperti halnya komponen, Custom Hooks harus bersifat murni. Bayangkan kode kustom Hooks sebagai bagian dari badan komponen Anda!

Karena Custom Hooks di-render ulang bersama komponen Anda, mereka selalu menerima prop dan state terbaru. Untuk mengetahui apa artinya, pertimbangkan contoh ruang obrolan ini. Ubah URL server atau ruang obrolannya:

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

export default function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useEffect(() => {
    const options = {
      serverUrl: serverUrl,
      roomId: roomId
    };
    const connection = createConnection(options);
    connection.on('message', (msg) => {
      showNotification('New message: ' + msg);
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, serverUrl]);

  return (
    <>
      <label>
        Server URL:
        <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

Ketika Anda mengubah serverUrl atau roomId, Effect “bereaksi” terhadap perubahan Anda dan disinkronkan ulang. Anda dapat mengetahui dari pesan konsol bahwa obrolan terhubung kembali setiap kali Anda mengubah dependensi Effect Anda.

Sekarang pindahkan kode Effect ke dalam Custom Hook:

export function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
showNotification('New message: ' + msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]);
}

Ini memungkinkan komponen ChatRoom Anda memanggil Custom Hook tanpa perlu mengkhawatirkan cara kerjanya di dalam:

export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});

return (
<>
<label>
Server URL:
<input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
</label>
<h1>Welcome to the {roomId} room!</h1>
</>
);
}

Ini terlihat jauh lebih sederhana! (Tapi tetap melakukan hal yang sama.)

Perhatikan bahwa logika tetap merespon perubahan prop dan state. Coba edit URL server atau ruang yang dipilih:

import { useState } from 'react';
import { useChatRoom } from './useChatRoom.js';

export default function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl
  });

  return (
    <>
      <label>
        Server URL:
        <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

Perhatikan bagaimana Anda mengambil nilai pengembalian dari satu Hook:

export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...

dan meneruskannya sebagai masukan ke Hook lain:

export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...

Setiap kali komponen ChatRoom Anda di-render ulang, komponen roomId dan serverUrl terbaru diteruskan ke Hook Anda. Inilah sebabnya Effect Anda terhubung kembali ke obrolan setiap kali nilainya berbeda setelah render ulang. (Jika Anda pernah bekerja dengan perangkat lunak pemrosesan audio atau video, merantai Hooks seperti ini mungkin mengingatkan Anda pada efek visual atau audio yang saling terkait. Seolah-olah output dari useState “diumpankan ke dalam” input dari useChatRoom.)

Passing event handlers to custom Hooks

Dalam Pengembangan

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

Saat Anda mulai menggunakan useChatRoom di lebih banyak komponen, Anda mungkin ingin membiarkan komponen menyesuaikan perilakunya. Sebagai contoh, saat ini, logika tentang apa yang harus dilakukan ketika sebuah pesan datang di-hardcode di dalam Hook:

export function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
showNotification('New message: ' + msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]);
}

Katakanlah Anda ingin memindahkan logika ini kembali ke komponen Anda:

export default function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

useChatRoom({
roomId: roomId,
serverUrl: serverUrl,
onReceiveMessage(msg) {
showNotification('New message: ' + msg);
}
});
// ...

Untuk membuatnya berfungsi, ubah custom Hook Anda untuk menerima onReceiveMessage sebagai salah satu opsi bernama:

export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
onReceiveMessage(msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl, onReceiveMessage]); // ✅ All dependencies declared
}

Ini akan berhasil, tetapi ada satu peningkatan lagi yang dapat Anda lakukan ketika custom Hook Anda menerima event handler.

Menambahkan dependensi pada onReceiveMessage tidak ideal karena akan menyebabkan chat terhubung kembali setiap kali komponen di-render ulang. Bungkus event handler ini ke dalam Effect Event untuk menghapusnya dari dependensi:

import { useEffect, useEffectEvent } from 'react';
// ...

export function useChatRoom({ serverUrl, roomId, onReceiveMessage }) {
const onMessage = useEffectEvent(onReceiveMessage);

useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
connection.on('message', (msg) => {
onMessage(msg);
});
return () => connection.disconnect();
}, [roomId, serverUrl]); // ✅ Semua dependensi dideklarasikan
}

Sekarang obrolan tidak akan terhubung kembali setiap kali komponen ChatRoom di-render ulang. Berikut adalah demo yang berfungsi penuh untuk meneruskan event handler ke custom Hook yang dapat Anda mainkan:

import { useState } from 'react';
import { useChatRoom } from './useChatRoom.js';
import { showNotification } from './notifications.js';

export default function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl,
    onReceiveMessage(msg) {
      showNotification('New message: ' + msg);
    }
  });

  return (
    <>
      <label>
        Server URL:
        <input value={serverUrl} onChange={e => setServerUrl(e.target.value)} />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

Perhatikan bagaimana Anda tidak perlu lagi mengetahui bagaimana useChatRoom berfungsi untuk menggunakannya. Anda bisa menambahkannya ke komponen lain, meneruskan opsi lain, dan itu akan bekerja dengan cara yang sama. Itulah kekuatan custom Hooks.

Kapan menggunakan custom Hooks

Anda tidak perlu mengekstrak custom Hook untuk setiap bit kode yang duplikat. Beberapa duplikasi memang diperbolehkan. Misalnya, mengekstrak Hook useFormInput untuk membungkus satu panggilan useState seperti sebelumnya mungkin tidak diperlukan.

Namun, kapan pun Anda menulis sebuah Effect, pertimbangkan apakah akan lebih jelas untuk juga membungkusnya dalam sebuah custom Hook. Anda seharusnya tidak terlalu sering menggunakan Effect, jadi jika Anda menulisnya, itu berarti Anda perlu “keluar dari React” untuk menyinkronkan dengan beberapa sistem eksternal atau untuk melakukan sesuatu yang tidak memiliki API bawaan di React. Membungkusnya menjadi custom Hook memungkinkan Anda mengomunikasikan maksud Anda dengan tepat dan bagaimana data mengalir melewatinya.

Misalnya, pertimbangkan sebuah komponen ShippingForm yang menampilkan dua dropdown: satu menampilkan daftar kota, dan lainnya menampilkan daftar area di kota yang dipilih. Anda mungkin akan memulai dengan kode yang terlihat seperti ini:

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
// Effect ini mengambil data kota untuk suatu negara
useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]);

const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);
// Effect ini mengambil data area untuk kota yang dipilih
useEffect(() => {
if (city) {
let ignore = false;
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
return () => {
ignore = true;
};
}
}, [city]);

// ...

Meskipun kode ini cukup berulang, memisahkan Efek ini satu sama lain adalah benar. Keduanya menyinkronkan dua hal yang berbeda, sehingga Anda tidak boleh menggabungkannya menjadi satu Efek. Sebagai gantinya, Anda dapat menyederhanakan komponen ShippingForm di atas dengan mengekstrak logika umum di antara mereka ke dalam useData Hook Anda sendiri:

function useData(url) {
const [data, setData] = useState(null);
useEffect(() => {
if (url) {
let ignore = false;
fetch(url)
.then(response => response.json())
.then(json => {
if (!ignore) {
setData(json);
}
});
return () => {
ignore = true;
};
}
}, [url]);
return data;
}

Sekarang Anda dapat mengganti kedua Efek di komponen ShippingForm dengan panggilan ke useData:

function ShippingForm({ country }) {
const cities = useData(`/api/cities?country=${country}`);
const [city, setCity] = useState(null);
const areas = useData(city ? `/api/areas?city=${city}` : null);
// ...

Mengekstrak custom Hook membuat aliran data menjadi eksplisit. Anda memasukkan url dan Anda mengeluarkan data. Dengan “menyembunyikan” Efek Anda di dalam useData, Anda juga mencegah seseorang yang bekerja pada komponen ShippingForm menambahkan dependensi yang tidak diperlukan ke dalamnya. Seiring waktu, sebagian besar Efek aplikasi Anda akan berada di dalam custom Hooks.

Pendalaman

Tetap fokuskan custom Hooks Anda pada kasus penggunaan tingkat tinggi yang konkret

Mulailah dengan memilih nama untuk custom Hook Anda. Jika Anda kesulitan memilih nama yang jelas, itu mungkin berarti Effect Anda terlalu terkait dengan logika komponen Anda yang lain, dan belum siap untuk diekstrak.

Idealnya, nama custom Hook Anda harus cukup jelas sehingga bahkan orang yang tidak sering menulis kode dapat memiliki tebakan yang baik tentang apa yang dilakukan oleh custom Hook Anda, apa yang diperlukan, dan apa yang dikembalikan:

  • useData(url)
  • useImpressionLog(eventName, extraData)
  • useChatRoom(options)

Ketika Anda melakukan sinkronisasi dengan sistem eksternal, nama custom Hook Anda mungkin lebih teknis dan menggunakan jargon yang spesifik untuk sistem itu. Itu bagus selama jelas bagi orang yang akrab dengan sistem tersebut:

  • useMediaQuery(query)
  • useSocket(url)
  • useIntersectionObserver(ref, options)

Tetap fokuskan custom Hooks pada kasus penggunaan tingkat tinggi yang konkret. Hindari membuat dan menggunakan custom “lifecycle” Hooks yang bertindak sebagai pembungkus alternatif dan praktis untuk API useEffect itu sendiri:

  • 🔴 useMount(fn)
  • 🔴 useEffectOnce(fn)
  • 🔴 useUpdateEffect(fn)

Sebagai contoh, Hook useMount ini mencoba memastikan beberapa kode hanya berjalan “pada mount”:

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

// 🔴 Hindari: menggunakan custom "lifecycle" Hooks
useMount(() => {
const connection = createConnection({ roomId, serverUrl });
connection.connect();

post('/analytics/event', { eventName: 'visit_chat' });
});
// ...
}

// 🔴 Hindari: membuat custom "lifecycle" Hooks
function useMount(fn) {
useEffect(() => {
fn();
}, []); // 🔴 React Hook useEffect memiliki dependensi yang hilang: 'fn'
}

Custom “lifecycle” Hook seperti useMount tidak cocok dengan paradigma React. Sebagai contoh, contoh kode ini memiliki kesalahan (tidak “merespons” terhadap perubahan roomId atau serverUrl) , tetapi linter tidak akan memperingatkan Anda tentang hal itu karena linter hanya memeriksa panggilan useEffect langsung. Itu tidak akan tahu tentang Hook Anda.

Jika Anda menulis sebuah Effect, mulailah dengan menggunakan API React secara langsung:

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

// ✅ Baik: dua raw Effects yang dipisahkan berdasarkan tujuan

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

useEffect(() => {
post('/analytics/event', { eventName: 'visit_chat', roomId });
}, [roomId]);

// ...
}

Kemudian, Anda dapat (tetapi tidak harus) mengekstrak custom Hooks untuk kasus penggunaan tingkat tinggi yang berbeda:

function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');

// ✅ Hebat: custom Hooks dengan nama sesuai tujuan
useChatRoom({ serverUrl, roomId });
useImpressionLog('visit_chat', { roomId });
// ...
}

Custom Hook yang baik membuat kode yang dipanggil lebih deklaratif dengan membatasi apa yang dilakukannya. Sebagai contoh, useChatRoom(options) hanya dapat terhubung ke ruang obrolan, sementara useImpressionLog(eventName, extraData) hanya dapat mengirim log impresi ke analitik. Jika API custom Hook Anda tidak membatasi kasus penggunaan dan sangat abstrak, dalam jangka panjang kemungkinan akan menimbulkan lebih banyak masalah daripada penyelesaiannya.

Custom Hooks membantu Anda bermigrasi ke pola yang lebih baik

Effects merupakan sebuah “escape hatch”: Anda menggunakannya ketika Anda perlu “keluar dari React” dan ketika tidak ada solusi bawaan yang lebih baik untuk kasus penggunaan Anda. Seiring waktu, tujuan tim React adalah untuk mengurangi jumlah Effects di aplikasi Anda seminimal mungkin dengan memberikan solusi yang lebih spesifik untuk masalah yang lebih spesifik. Membungkus Effects Anda dalam custom Hooks memudahkan untuk memutakhirkan kode Anda ketika solusi-solusi ini tersedia.

Mari kita kembali ke contoh ini:

import { useState, useEffect } from 'react';

export function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    function handleOnline() {
      setIsOnline(true);
    }
    function handleOffline() {
      setIsOnline(false);
    }
    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);
    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);
  return isOnline;
}

Dalam contoh di atas, useOnlineStatus diimplementasikan dengan sepasang useState dan useEffect. Namun, ini bukanlah solusi terbaik. Ada beberapa kasus tepi yang tidak dipertimbangkan. Misalnya, diasumsikan bahwa ketika komponen dipasang, isOnline sudah benar, tetapi hal ini mungkin salah jika jaringan sudah offline. Anda dapat menggunakan API browser navigator.onLine untuk memeriksanya, tetapi menggunakannya secara langsung tidak akan berhasil di server untuk menghasilkan HTML awal. Singkatnya, kode ini dapat diperbaiki.

Untungnya, React 18 menyertakan API khusus yang disebut useSyncExternalStore yang menangani semua masalah ini untuk Anda. Berikut adalah bagaimana Hook useOnlineStatus Anda, ditulis ulang untuk memanfaatkan API baru ini:

import { useSyncExternalStore } from 'react';

function subscribe(callback) {
  window.addEventListener('online', callback);
  window.addEventListener('offline', callback);
  return () => {
    window.removeEventListener('online', callback);
    window.removeEventListener('offline', callback);
  };
}

export function useOnlineStatus() {
  return useSyncExternalStore(
    subscribe,
    () => navigator.onLine, // Bagaimana cara mendapatkan nilai di sisi klien
    () => true // Bagaimana cara mendapatkan nilai di sisi server
  );
}

Perhatikan bagaimana Anda tidak perlu mengubah komponen apa pun untuk melakukan migrasi ini:

function StatusBar() {
const isOnline = useOnlineStatus();
// ...
}

function SaveButton() {
const isOnline = useOnlineStatus();
// ...
}

Ini adalah alasan lain mengapa membungkus Effects di custom Hooks seringkali bermanfaat:

  1. Anda membuat aliran data ke dan dari Effects Anda sangat eksplisit.
  2. Anda membiarkan komponen Anda fokus pada maksud daripada pada implementasi yang tepat dari Effects Anda.
  3. Saat React menambahkan fitur baru, Anda dapat menghapus Effects tersebut tanpa mengubah komponen apa pun.

Mirip dengan sistem desain, Anda mungkin merasa terbantu untuk mulai mengekstraksi idiom umum dari komponen aplikasi Anda ke dalam custom Hooks. Ini akan membuat kode komponen Anda tetap fokus pada maksud, dan memungkinkan Anda menghindari menulis Effects mentah terlalu sering. Banyak custom Hooks yang sangat baik dikelola oleh komunitas React.

Pendalaman

Akankah React akan menyediakan solusi bawaan untuk pengambilan data?

Kami masih mengerjakan detailnya, tetapi kami berharap di masa mendatang, Anda akan menulis pengambilan data seperti ini:

import { use } from 'react'; // Belum tersedia!

function ShippingForm({ country }) {
const cities = use(fetch(`/api/cities?country=${country}`));
const [city, setCity] = useState(null);
const areas = city ? use(fetch(`/api/areas?city=${city}`)) : null;
// ...

Jika Anda menggunakan custom Hooks seperti useData di atas dalam aplikasi Anda, itu akan memerlukan lebih sedikit perubahan untuk bermigrasi ke pendekatan yang direkomendasikan daripada jika Anda menulis Effects mentah di setiap komponen secara manual. Namun, pendekatan lama akan tetap berfungsi dengan baik, jadi jika Anda merasa senang menulis Effects mentah, Anda dapat terus melakukannya.

Ada lebih dari satu cara untuk melakukannya

Katakanlah Anda ingin mengimplementasikan animasi fade-in dari awal menggunakan API browser requestAnimationFrame. Anda mungkin memulainya dengan sebuah Effect yang menyiapkan lingkaran animasi. Selama setiap frame animasi, Anda dapat mengubah opacity node DOM yang disimpan di ref hingga mencapai 1. Kode Anda mungkin dimulai seperti ini:

import { useState, useEffect, useRef } from 'react';

function Welcome() {
  const ref = useRef(null);

  useEffect(() => {
    const duration = 1000;
    const node = ref.current;

    let startTime = performance.now();
    let frameId = null;

    function onFrame(now) {
      const timePassed = now - startTime;
      const progress = Math.min(timePassed / duration, 1);
      onProgress(progress);
      if (progress < 1) {
        // Masih ada banyak frame yang perlu diwarnai
        frameId = requestAnimationFrame(onFrame);
      }
    }

    function onProgress(progress) {
      node.style.opacity = progress;
    }

    function start() {
      onProgress(0);
      startTime = performance.now();
      frameId = requestAnimationFrame(onFrame);
    }

    function stop() {
      cancelAnimationFrame(frameId);
      startTime = null;
      frameId = null;
    }

    start();
    return () => stop();
  }, []);

  return (
    <h1 className="welcome" ref={ref}>
      Welcome
    </h1>
  );
}

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Remove' : 'Show'}
      </button>
      <hr />
      {show && <Welcome />}
    </>
  );
}

Untuk membuat komponen lebih mudah dibaca, Anda dapat mengekstrak logika ke dalam custom Hook useFadeIn:

import { useState, useEffect, useRef } from 'react';
import { useFadeIn } from './useFadeIn.js';

function Welcome() {
  const ref = useRef(null);

  useFadeIn(ref, 1000);

  return (
    <h1 className="welcome" ref={ref}>
      Welcome
    </h1>
  );
}

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Remove' : 'Show'}
      </button>
      <hr />
      {show && <Welcome />}
    </>
  );
}

Anda dapat mempertahankan kode useFadeIn apa adanya, tetapi Anda juga dapat merestrukturisasi lebih lanjut. Misalnya, Anda dapat mengekstrak logika untuk menyiapkan loop animasi dari useFadeIn menjadi custom Hook useAnimationLoop:

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

export function useFadeIn(ref, duration) {
  const [isRunning, setIsRunning] = useState(true);

  useAnimationLoop(isRunning, (timePassed) => {
    const progress = Math.min(timePassed / duration, 1);
    ref.current.style.opacity = progress;
    if (progress === 1) {
      setIsRunning(false);
    }
  });
}

function useAnimationLoop(isRunning, drawFrame) {
  const onFrame = useEffectEvent(drawFrame);

  useEffect(() => {
    if (!isRunning) {
      return;
    }

    const startTime = performance.now();
    let frameId = null;

    function tick(now) {
      const timePassed = now - startTime;
      onFrame(timePassed);
      frameId = requestAnimationFrame(tick);
    }

    tick();
    return () => cancelAnimationFrame(frameId);
  }, [isRunning]);
}

Namun, Anda tidak harus melakukan itu. Seperti halnya fungsi biasa, pada akhirnya Anda yang memutuskan di mana harus menggambar batas antara berbagai bagian kode Anda. Anda juga bisa mengambil pendekatan yang sangat berbeda. Alih-alih mempertahankan logika dalam Effect, Anda dapat memindahkan sebagian besar logika imperatif ke dalam JavaScript class:

import { useState, useEffect } from 'react';
import { FadeInAnimation } from './animation.js';

export function useFadeIn(ref, duration) {
  useEffect(() => {
    const animation = new FadeInAnimation(ref.current);
    animation.start(duration);
    return () => {
      animation.stop();
    };
  }, [ref, duration]);
}

Effects memungkinkan Anda menghubungkan React dengan sistem eksternal. Semakin banyak koordinasi antara Effects yang dibutuhkan (misalnya, untuk menghubungkan beberapa animasi), semakin masuk akal untuk mengekstrak logika tersebut dari Effects dan Hooks sepenuhnya seperti yang ditunjukkan di sandbox di atas. Kemudian, kode yang Anda ekstrak menjadi “sistem eksternal”. Ini memungkinkan Effects Anda tetap sederhana karena mereka hanya perlu mengirim pesan ke sistem yang telah Anda pindahkan ke luar React.

Contoh di atas mengasumsikan bahwa logika fade-in perlu ditulis dalam JavaScript. Namun, animasi fade-in khusus ini lebih sederhana dan jauh lebih efisien untuk diterapkan dengan Animasi CSS: biasa

.welcome {
  color: white;
  padding: 50px;
  text-align: center;
  font-size: 50px;
  background-image: radial-gradient(circle, rgba(63,94,251,1) 0%, rgba(252,70,107,1) 100%);

  animation: fadeIn 1000ms;
}

@keyframes fadeIn {
  0% { opacity: 0; }
  100% { opacity: 1; }
}

Terkadang, Anda bahkan tidak perlu menggunakan Hook!

Rekap

  • Custom Hook memungkinkan Anda untuk berbagi logika antar komponen.
  • Custom Hook harus diberi nama yang dimulai dengan use diikuti oleh huruf kapital.
  • Custom Hook hanya berbagi logika yang berhubungan dengan state, bukan state itu sendiri.
  • Anda dapat meneruskan nilai reaktif dari satu Hook ke Hook lainnya, dan nilai tersebut tetap up-to-date.
  • Semua Hook dijalankan ulang setiap kali komponen Anda di-render ulang.
  • Kode dari custom Hook Anda harus bersifat murni, seperti kode komponen Anda.
  • Bungkus event handler yang diterima oleh custom Hook menjadi Effect Events.
  • Jangan membuat Custom Hook seperti useMount. Jaga agar tujuan mereka tetap spesifik.
  • Itu terserah Anda bagaimana dan di mana Anda memilih batasan dalam kode Anda.

Tantangan 1 dari 5:
Ekstrak custom Hook useCounter

Komponen ini menggunakan variabel state dan Effect untuk menampilkan angka yang bertambah setiap detik. Ekstrak logika ini menjadi custom Hook bernama useCounter. Tujuan Anda adalah membuat implementasi komponen Counter terlihat persis seperti ini:

export default function Counter() {
const count = useCounter();
return <h1>Seconds passed: {count}</h1>;
}

Anda perlu menulis custom Hook Anda di dalam file useCounter.js dan mengimpornya ke file Counter.js.

import { useState, useEffect } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    const id = setInterval(() => {
      setCount(c => c + 1);
    }, 1000);
    return () => clearInterval(id);
  }, []);
  return <h1>Seconds passed: {count}</h1>;
}