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.

You will learn

  • 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!

Note

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!

Deep Dive

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 apapun, 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 Custom Hooks akan dijalankan kembali setiap kali komponen Anda dirender ulang. Oleh karena itu, seperti halnya komponen, Custom Hooks harus bersifat murni. Bayangkan kode Custom 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

Under Construction

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]); // ✅ All dependencies declared
}

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.

When to use custom Hooks

You don’t need to extract a custom Hook for every little duplicated bit of code. Some duplication is fine. For example, extracting a useFormInput Hook to wrap a single useState call like earlier is probably unnecessary.

However, whenever you write an Effect, consider whether it would be clearer to also wrap it in a custom Hook. You shouldn’t need Effects very often, so if you’re writing one, it means that you need to “step outside React” to synchronize with some external system or to do something that React doesn’t have a built-in API for. Wrapping it into a custom Hook lets you precisely communicate your intent and how the data flows through it.

For example, consider a ShippingForm component that displays two dropdowns: one shows the list of cities, and another shows the list of areas in the selected city. You might start with some code that looks like this:

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
// This Effect fetches cities for a country
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);
// This Effect fetches areas for the selected city
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]);

// ...

Although this code is quite repetitive, it’s correct to keep these Effects separate from each other. They synchronize two different things, so you shouldn’t merge them into one Effect. Instead, you can simplify the ShippingForm component above by extracting the common logic between them into your own useData Hook:

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;
}

Now you can replace both Effects in the ShippingForm components with calls to 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);
// ...

Extracting a custom Hook makes the data flow explicit. You feed the url in and you get the data out. By “hiding” your Effect inside useData, you also prevent someone working on the ShippingForm component from adding unnecessary dependencies to it. With time, most of your app’s Effects will be in custom Hooks.

Deep Dive

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');

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

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

// 🔴 Avoid: creating custom "lifecycle" Hooks
function useMount(fn) {
useEffect(() => {
fn();
}, []); // 🔴 React Hook useEffect has a missing dependency: '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');

// ✅ Good: two raw Effects separated by purpose

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');

// ✅ Great: custom Hooks named after their purpose
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, // How to get the value on the client
    () => true // How to get the value on the 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.

Deep Dive

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'; // Not available yet!

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.

There is more than one way to do it

Let’s say you want to implement a fade-in animation from scratch using the browser requestAnimationFrame API. You might start with an Effect that sets up an animation loop. During each frame of the animation, you could change the opacity of the DOM node you hold in a ref until it reaches 1. Your code might start like this:

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) {
        // We still have more frames to paint
        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 />}
    </>
  );
}

To make the component more readable, you might extract the logic into a useFadeIn custom Hook:

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 />}
    </>
  );
}

You could keep the useFadeIn code as is, but you could also refactor it more. For example, you could extract the logic for setting up the animation loop out of useFadeIn into a custom useAnimationLoop Hook:

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]);
}

However, you didn’t have to do that. As with regular functions, ultimately you decide where to draw the boundaries between different parts of your code. You could also take a very different approach. Instead of keeping the logic in the Effect, you could move most of the imperative logic inside a 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 let you connect React to external systems. The more coordination between Effects is needed (for example, to chain multiple animations), the more it makes sense to extract that logic out of Effects and Hooks completely like in the sandbox above. Then, the code you extracted becomes the “external system”. This lets your Effects stay simple because they only need to send messages to the system you’ve moved outside React.

The examples above assume that the fade-in logic needs to be written in JavaScript. However, this particular fade-in animation is both simpler and much more efficient to implement with a plain CSS Animation:

.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; }
}

Sometimes, you don’t even need a Hook!

Recap

  • Custom Hooks let you share logic between components.
  • Custom Hooks must be named starting with use followed by a capital letter.
  • Custom Hooks only share stateful logic, not state itself.
  • You can pass reactive values from one Hook to another, and they stay up-to-date.
  • All Hooks re-run every time your component re-renders.
  • The code of your custom Hooks should be pure, like your component’s code.
  • Wrap event handlers received by custom Hooks into Effect Events.
  • Don’t create custom Hooks like useMount. Keep their purpose specific.
  • It’s up to you how and where to choose the boundaries of your code.

Challenge 1 of 5:
Extract a useCounter Hook

This component uses a state variable and an Effect to display a number that increments every second. Extract this logic into a custom Hook called useCounter. Your goal is to make the Counter component implementation look exactly like this:

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

You’ll need to write your custom Hook in useCounter.js and import it into the Counter.js file.

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>;
}