@chalidade/react-hooks
React hooks kecil yang aman untuk SSR dan teruji. Setiap hook menjawab satu masalah yang sering salah ditulis sendiri: stale closure, listener yang lupa dilepas, atau hydration mismatch.
pnpm add @chalidade/react-hooksBelum dipublish
Package ini belum dipublish ke npm. Sementara ini, salin source-nya dari GitHub.
Membutuhkan React 18 atau lebih baru (useSyncExternalStore).
Ringkasan
| Hook | Untuk apa |
|---|---|
useDebouncedValue | Nilai yang menunggu input berhenti (search) |
useDebouncedCallback | Fungsi debounce yang stabil antar render (autosave) |
useThrottledCallback | Maksimal sekali per interval (scroll, resize, drag) |
useOnline | Status koneksi untuk banner offline |
useMediaQuery | Responsif di JS tanpa hydration mismatch |
useLocalStorage | State yang tersimpan dan sinkron antar tab |
useEventListener | Listener yang otomatis dilepas |
useOnClickOutside | Menutup dropdown/popover saat klik di luar |
useIntersectionObserver | Lazy load, infinite scroll, animasi masuk |
usePrevious | Nilai di render sebelumnya |
useLatest | Ref berisi nilai terbaru (anti stale closure) |
useIsomorphicLayoutEffect | useLayoutEffect tanpa peringatan di server |
useDebouncedValue
const [input, setInput] = useState("");
const query = useDebouncedValue(input, 300);
useEffect(() => {
if (query) void search(query);
}, [query]);useDebouncedCallback
Identitas fungsinya tetap sama antar render, tapi selalu memanggil callback terbaru. Panggilan yang menunggu dibatalkan saat komponen unmount, jadi tidak ada setState setelah unmount.
const save = useDebouncedCallback((draft: string) => api.save(draft), 1000);
<textarea onChange={(e) => save(e.target.value)} onBlur={() => save.flush()} />useThrottledCallback
Panggilan pertama langsung jalan, sisanya di jendela yang sama digabung menjadi satu panggilan di akhir jendela dengan argumen terbaru. Cocok untuk event yang terus menyala selama interaksi.
const onScroll = useThrottledCallback(() => setProgress(readScrollProgress()), 100);
useEventListener(window, "scroll", onScroll, { passive: true });useOnline
const online = useOnline();
return online ? null : <p role="status">Kamu sedang offline. Perubahan disimpan lokal.</p>;useMediaQuery
const isMobile = useMediaQuery("(max-width: 640px)");
const prefersReducedMotion = useMediaQuery("(prefers-reduced-motion: reduce)");Di server nilainya false (atau argumen kedua). Karena memakai useSyncExternalStore, React memakai nilai server saat hydration lalu memperbaruinya, jadi tidak ada hydration mismatch.
useLocalStorage
const [theme, setTheme, resetTheme] = useLocalStorage<"light" | "dark">("theme", "light");- Komponen lain dengan key yang sama ikut ter-update.
- Tab lain ikut ter-update lewat event
storage. - Data rusak atau storage yang diblokir (mode privat) tidak membuat aplikasi crash: nilai kembali ke
initialValue.
useEventListener
useEventListener(window, "keydown", (e) => {
if (e.key === "Escape") close();
});
const ref = useRef<HTMLDivElement>(null);
useEventListener(ref, "scroll", onScroll, { passive: true });Tipe event mengikuti target (KeyboardEvent untuk keydown, dst.).
useOnClickOutside
const ref = useRef<HTMLDivElement>(null);
useOnClickOutside(ref, () => setOpen(false));
return <div ref={ref}>{open && <Menu />}</div>;Memakai pointerdown supaya juga bekerja di layar sentuh.
useIntersectionObserver
const { ref, isIntersecting } = useIntersectionObserver({ rootMargin: "200px", once: true });
return <img ref={ref} src={isIntersecting ? src : undefined} alt={alt} />;usePrevious
const prevCount = usePrevious(count);
const direction = prevCount === undefined ? null : count > prevCount ? "naik" : "turun";useLatest
const onTickRef = useLatest(onTick);
useEffect(() => {
const id = setInterval(() => onTickRef.current(), 1000);
return () => clearInterval(id);
}, []); // interval tidak perlu dipasang ulang saat onTick berubahBelajar dari source-nya
Coba tulis useLocalStorage versi useState + useEffect sendiri, lalu jalankan test di packages/react-hooks. Kasus mana yang gagal? (Petunjuk: dua komponen dengan key yang sama, dan render pertama di server.)
Konsep terkait: 07 Framework, 17 Browser APIs, 18 Rendering Strategies.