Custom React Hook-ovi: Izvuci Logiku, Gradi Višekratne Apstrakcije
Nauči kako da pišeš sopstvene custom hook-ove da bi izvukao višekratnu logiku iz komponenti, održao kod čistim i gradio apstrakcije koje razdvajaju junior od mid-level developera.
Custom React Hook-ovi: Izvuci Logiku, Gradi Višekratne Apstrakcije
TL;DR — Ključni Uvidi
- Custom hook je JavaScript funkcija čije ime počinje sa
usei koja interno poziva druge hook-ove- Omogućavaju ti da izvučeš stateful logiku iz komponenti i koristiš je bilo gde
- Custom hook-ovi ne dele state — svaki poziv kreira svoju nezavisnu instancu
- Primarni su alat za održavanje komponenti lean i fokusiranih na renderovanje
- Pisanje custom hook-ova je veština koja najjasnije razdvaja junior od mid-level React developera
Zašto custom hook-ovi postoje
Do sad u ovoj seriji, naučio si ugrađene hook-ove: useState, useEffect, useCallback, useMemo, useRef i useContext. Znaš kako svaki od njih radi individualno. Ali prave aplikacije ih kombinuju — ponekad u istom paternu, ponovljenom kroz desetak komponenti.
Zamislimo da imaš pet različitih stranica koje sve povlače podatke sa API-ja. Svaka ima sopstveni isLoading, error i data state. Svaka ima isti useEffect sa istim cleanup paternom iz State Management i Životni Ciklus. Kopiraš i lepiš istih 20 linija koda u svaku komponentu.
Tačno to custom hook-ovi rešavaju.
Custom hook ti omogućava da izvučeš tu ponovljenu logiku u jednu funkciju — i koristiš je jednom linijom koda bilo gde u aplikaciji.
Anatomija custom hook-a
Custom hook je samo funkcija koja:
- Ima ime koje počinje sa
use(ovo nije opciono — React-ova lint pravila zavise od toga) - Interno poziva jedan ili više ugrađenih hook-ova
- Vraća sve što konzument treba — vrednosti, setter-e, funkcije, ili bilo koju kombinaciju
To je sve. Nema posebnog API-ja, nema registracije, nema klase za proširivanje. Samo funkcija.
Primer 1: useFetch — Apstrakcija za povlačenje podataka
Ovo je najčešći custom hook koji ćeš pisati u bilo kom B2B SaaS projektu. Umesto ponavljanja fetch + loading + error patterna u svakoj komponenti, jednom ga izvlačimo.
// hooks/useFetch.ts
import { useState, useEffect, useCallback } from "react";
interface FetchState<T> {
data: T | null;
isLoading: boolean;
error: string | null;
refetch: () => void;
}
export function useFetch<T>(url: string): FetchState<T> {
const [data, setData] = useState<T | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [trigger, setTrigger] = useState(0);
useEffect(() => {
let cancelled = false;
async function fetchData() {
try {
setIsLoading(true);
setError(null);
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP greška: ${res.status}`);
const json = await res.json();
if (!cancelled) setData(json);
} catch (err) {
if (!cancelled) setError(err instanceof Error ? err.message : "Nepoznata greška");
} finally {
if (!cancelled) setIsLoading(false);
}
}
fetchData();
return () => { cancelled = true; };
}, [url, trigger]);
const refetch = useCallback(() => setTrigger((t) => t + 1), []);
return { data, isLoading, error, refetch };
}Sada svaka komponenta koja treba da povuče podatke koristi ovo umesto toga:
// components/ProductDetail.tsx
import { useFetch } from "@/hooks/useFetch";
interface Product {
id: string;
name: string;
description: string;
price: number;
}
export function ProductDetail({ productId }: { productId: string }) {
const { data: product, isLoading, error, refetch } = useFetch<Product>(
`/api/products/${productId}`
);
if (isLoading) {
return (
<div className="animate-pulse rounded-xl bg-gray-100 p-6 h-48" />
);
}
if (error) {
return (
<div className="rounded-xl border border-red-200 bg-red-50 p-6">
<p className="text-sm text-red-600">{error}</p>
<button
onClick={refetch}
className="mt-3 text-sm font-medium text-red-700 underline"
>
Pokušaj ponovo
</button>
</div>
);
}
if (!product) return null;
return (
<div className="rounded-xl border border-gray-200 p-6 shadow-sm">
<h2 className="text-xl font-semibold">{product.name}</h2>
<p className="mt-2 text-sm text-gray-600">{product.description}</p>
<p className="mt-4 text-lg font-bold">{product.price} RSD</p>
</div>
);
}Komponenta je sada 40 linija čiste logike renderovanja. Sva složenost fetch-a živi na jednom mestu — useFetch.ts — i može biti ažurirana jednom za sve konzumente.
Primer 2: useDebounce — Kontrola frekvencije unosa
U B2B aplikacijama, search input-i koji okidaju API pozive pri svakom pritisku tastera su čest problem sa performansama i troškovima. useDebounce hook odlaže ažuriranje vrednosti dok korisnik ne prestane da kuca.
// hooks/useDebounce.ts
import { useState, useEffect } from "react";
export function useDebounce<T>(value: T, delay: number = 400): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}Upotreba je jedna linija:
// components/ProductSearch.tsx
import { useState } from "react";
import { useDebounce } from "@/hooks/useDebounce";
import { useFetch } from "@/hooks/useFetch";
interface SearchResult {
id: string;
name: string;
}
export function ProductSearch() {
const [query, setQuery] = useState("");
const debouncedQuery = useDebounce(query, 400);
const { data: results, isLoading } = useFetch<SearchResult[]>(
`/api/products/search?q=${encodeURIComponent(debouncedQuery)}`
);
return (
<div className="space-y-3">
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Pretraži proizvode..."
className="w-full rounded-lg border border-gray-300 px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
{isLoading && <p className="text-xs text-gray-400">Pretraživanje...</p>}
{results && (
<ul className="divide-y divide-gray-100 rounded-lg border border-gray-200">
{results.map((r) => (
<li key={r.id} className="px-4 py-2 text-sm">{r.name}</li>
))}
</ul>
)}
</div>
);
}Primetimo da komponujemo dva custom hook-a — useDebounce i useFetch — zajedno. Ova mogućnost kompozicije je jedna od najmoćnijih osobina patterna custom hook-ova.
Pravila custom hook-ova
Custom hook-ovi prate ista pravila kao i ugrađeni hook-ovi:
Pravilo 1: Pozivaj hook-ove samo na najvišem nivou. Nikada unutar petlji, uslova ili ugneždenih funkcija. React se oslanja na to da je redosled poziva hook-ova konzistentan između rendera.
Pravilo 2: Pozivaj hook-ove samo iz React funkcija. Ili iz React komponente ili iz drugog custom hook-a. Nikada iz obične utility funkcije.
Pravilo 3: Prefiks use je obavezan. Bez njega, React ne može da primeni gornja pravila putem ESLint-a, i drugi developeri neće znati da funkcija sadrži hook-ove.
Šta čini dobar custom hook
Nije svaka izvučena funkcija kandidat za hook. Evo testa koji primenjujemo u Atonize-u:
Izvuci u custom hook kada:
- Logika koristi jedan ili više ugrađenih hook-ova
- Ista logika se pojavljuje u dve ili više komponenti
- Logika je dovoljno složena da zamagli nameru renderovanja komponente
- Logika ima sopstveno smisleno ime koje opisuje šta radi, a ne kako radi (
useProductSearch, neuseFilterAndDebounce)
NE izvlači u custom hook kada:
- Logika je čista kalkulacija bez hook-ova (izvuci u regularnu utility funkciju umesto toga)
- Koristila bi se samo na jednom mestu i nije posebno složena
- Izvlačiš čisto radi izvlačenja — preuranjena apstrakcija je jednako štetna kao duplikacija
Česte Greške i Zamke
1. Pretpostavljanje da hook-ovi dele state
Svaki poziv custom hook-a kreira sopstveni nezavisni state. Ako ComponentA i ComponentB oba pozivaju useFetch("/api/products"), svaki dobija sopstveni data, isLoading i error — ne dele jedan fetch. Za deljeni server state, koristi biblioteku za keširanje poput TanStack Query.
2. Vraćanje nestabilnih referenci
Ako tvoj hook vraća funkcije ili objekte, memoizuj ih — inače konzumenti koji ih prosleđuju memo()-u ili koriste kao useEffect zavisnosti imaće probleme:
// ❌ Nova referenca na objekat pri svakom renderu
return { data, isLoading, refetch };
// ✅ Funkcije omotane u useCallback, objekat je ok jer konzumenti destrukturišu
const refetch = useCallback(() => setTrigger(t => t + 1), []);
return { data, isLoading, error, refetch };3. Imenovanje hook-ova kao utility funkcija
useData, useHelper, useStuff ne govore ništa. Imenuj hook-ove po domenskom konceptu koji enkapsuliraju: useAuth, useCartTotal, useProductSearch, useWindowSize. Dobro ime hook-a čini da komponenta čita kao obični engleski jezik.
Kuda ovo vodi
Custom hook-ovi su most između sirovih React primitiva i dobro arhitektuirane aplikacije. Kada tvoj tim izgradi deljenu biblioteku hook-ova — useFetch, useDebounce, usePermissions, usePagination — razvoj funkcionalnosti postaje dramatično brži i konzistentniji.
U sledećem tekstu, izlazimo izvan samog React-a i gledamo kako upravljati state-om koji treba da se deli kroz celu aplikaciju — istražujemo Globalni State Management u 2026. godini: Zustand vs Redux.
Često Postavljana Pitanja (FAQ)
Koja je razlika između custom hook-a i utility funkcije?
Utility funkcija je čista funkcija bez hook-ova — prima ulaze i vraća izlaze. Custom hook sadrži jedan ili više ugrađenih hook-ova i time ima pristup React-ovom sistemu state-a i životnog ciklusa. Ako tvoja funkcija ne poziva nijedan hook, treba da bude regularna utility funkcija u /utils ili /lib folderu, ne hook.
Mogu li custom hook-ovi da vraćaju JSX?
Tehnički da, ali ne bi trebalo. Hook koji vraća JSX je efektivno komponenta bez ugovora komponente. Ako trebaš da vratiš UI, napravi komponentu. Hook-ovi treba da vraćaju podatke, state i funkcije — ne markup.
Da li custom hook-ovi izazivaju re-rendere?
Da, indirektno. Kada se state unutar custom hook-a promeni, komponenta koja koristi taj hook se re-renderuje — isto kao da je state definisan direktno u komponenti. Custom hook-ovi ne dodaju nikakav dodatni overhead re-rendera.
Gde da smestim fajlove custom hook-ova u Next.js projektu?
Koristimo top-level /hooks direktorijum za deljene hook-ove koji se koriste kroz sve feature-e, i smještamo hook-ove specifične za feature pored komponente koja ih primarno koristi. Na primer, useProductSearch bi mogao da živi u /features/catalog/hooks/useProductSearch.ts umesto u globalnom /hooks folderu.
Serijal: React Masterclass
- 1
- 2
- 3
- 4
- 5Custom React Hook-ovi: Izvuci Logiku, Gradi Višekratne Apstrakcije (You are here)
- 6
- 7