useCallback
useCallback
je React Hook koji vam omogućava da keširate definiciju funkcije između ponovnih rendera.
const cachedFn = useCallback(fn, dependencies)
Reference
useCallback(fn, dependencies)
Pozovite useCallback
na vrhu vaše komponente kako biste keširali definiciju funkcije između ponovnih rendera:
import { useCallback } from 'react';
export default function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
Parametri
-
fn
: Funkcija koju želite da keširate. Može primiti bilo koje argumente i vratiti bilo koje vrednosti. React će vratiti (ne pozvati!) vašu funkciju nazad tokom inicijalnog rendera. U narednim renderima, React će vam dati istu funkciju ako sedependencies
nisu promenili od poslednjeg rendera. U suprotnom, vratiće vam funkciju koju ste prosledili tokom trenutnog rendera i sačuvati je za slučaj da se može iskoristiti kasnije. React neće pozvati vašu funkciju. Funkcija će vam biti vraćena kako biste odlučili kada i da li ćete je pozvati. -
dependencies
: Lista svih reaktivnih vrednosti referenciranih unutar kodafn
funkcije. Reaktivne vrednosti uključuju props-e, state i sve promenljive i funkcije deklarisane direktno unutar tela vaše komponente. Ako vam je linter konfigurisan za React, verifikovaće da li je svaka reaktivna vrednost ispravno specificirana kao zavisnost. Lista zavisnosti mora imati konstantan broj članova i biti napisana inline poput[dep1, dep2, dep3]
. React će uporediti svaku zavisnost sa njenom prethodnom vrednošću upotrebomObject.is
algoritma poređenja.
Povratne vrednosti
Prilikom inicijalnog rendera, useCallback
vraća fn
funkciju koju ste prosledili.
Tokom narednih rendera, vratiće ili već sačuvanu fn
funkciju iz prethodnog rendera (ako se zavisnosti nisu promenile), ili fn
funkciju koju ste prosledili u trenutnom renderu.
Upozorenja
useCallback
je Hook, pa ga možete pozvati samo na vrhu vaše komponente ili vaših Hook-ova. Ne možete ga pozvati unutar petlji i uslova. Ako vam je to potrebno, izdvojite novu komponentu i pomerite state u nju.- React neće odbaciti keširanu funkciju osim ako ne postoji poseban razlog za tako nešto. Na primer, u toku razvoja, React odbacuje keš kada izmenite fajl vaše komponente. U toku razvoja i u produkciji, React će odbaciti keš ako se vaša komponenta suspenduje tokom inicijalnog montiranja. U budućnosti, React može dodati nove funkcionalnosti koje koriste odbacivanje keša—na primer, ako React doda ugrađenu podršku za virtuelizovane liste u budućnosti, imalo bi smisla odbaciti keš za članove koji izlaze izvan vidnog polja virtuelizovane tabele. Ovo bi trebalo ispuniti vaša očekivanja ako se uzdate u
useCallback
za optimizaciju performansi. Inače, state promenljiva ili ref mogu biti prikladnija rešenja.
Upotreba
Preskakanje ponovnog renderovanja komponenata
Kada optimizujete performanse renderovanja, ponekad ćete trebati da keširate funkcije koje prosleđujete dečjim komponentama. Hajde prvo da pogledamo sintaksu kako bi se to moglo uraditi i da vidimo u kojim slučajevima je to korisno.
Da biste keširali funkciju između ponovnih rendera vaše komponente, obmotajte njenu definiciju sa useCallback
Hook-om:
import { useCallback } from 'react';
function ProductPage({ productId, referrer, theme }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
// ...
Potrebno je proslediti dve stvari u useCallback
:
- Definiciju funkcije koju želite keširati između ponovnih rendera.
- Listu zavisnosti koja uključuje svaku vrednost unutar vaše komponente koja se koristi unutar te funkcije.
Prilikom inicijalnog rendera, povratna funkcija koju dobijate iz useCallback
-a će biti funkcija koju ste prosledili.
U narednim renderima, React će porediti zavisnosti sa zavisnostima koje ste prosledili tokom prethodnog rendera. Ako se nijedna zavisnost nije promenila (poređenjem sa Object.is
), useCallback
će vratiti istu funkciju kao i pre. U suprotnom, useCallback
će vratiti funkciju koju ste prosledili u ovom renderu.
Drugim rečima, useCallback
kešira funkciju između ponovnih rendera dok joj se zavisnosti ne promene.
Prođimo kroz primer da vidimo kada je ovo korisno.
Recimo da prosleđujete handleSubmit
funkciju iz ProductPage
u ShippingForm
komponentu:
function ProductPage({ productId, referrer, theme }) {
// ...
return (
<div className={theme}>
<ShippingForm onSubmit={handleSubmit} />
</div>
);
Primetili ste da promena theme
prop-a zamrzava aplikaciju na sekund, ali ako uklonite <ShippingForm />
iz JSX-a, sve radi brzo. Ovo vam govori da nije loše probati da optimizujete ShippingForm
komponentu.
Po default-u, kada se komponenta ponovo renderuje, React rekurzivno ponovo renderuje svu njenu decu. Zbog toga, kada se ProductPage
ponovo renderuje sa novom theme
, i ShippingForm
komponenta se takođe ponovo renderuje. Ovo je u redu za komponente koje ne zahtevaju mnogo proračuna za ponovno renderovanje. Ali, ako ste potvrdili da je ponovno renderovanje sporo, možete reći ShippingForm
komponenti da preskoči renderovanje kada su njeni props-i isti kao i u prethodnom renderu, tako što ćete je obmotati sa memo
:
import { memo } from 'react';
const ShippingForm = memo(function ShippingForm({ onSubmit }) {
// ...
});
Sa ovom promenom, ShippingForm
će preskočiti ponovno renderovanje ako su joj svi props-i isti kao i u poslednjem renderu. Ovde keširanje funkcije postaje važno! Recimo da ste definisali handleSubmit
bez useCallback
:
function ProductPage({ productId, referrer, theme }) {
// Svaki put kad se theme promeni, ovo će biti drugačija funkcija...
function handleSubmit(orderDetails) {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}
return (
<div className={theme}>
{/* ... pa props-i ShippingForm-a nikad neće biti isti i svaki put će se ponovo renderovati */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
U JavaScript-u, function () {}
ili () => {}
uvek kreira drugačiju funkciju, slično kao što {}
literal objekta uvek kreira novi objekat. Obično ovo ne bi bio problem, ali ovo znači da props-i ShippingForm
-a nikad neće biti isti i da vaša memo
optimizacija ne radi. Ovde useCallback
postaje korisna:
function ProductPage({ productId, referrer, theme }) {
// Reci React-u da kešira tvoju funkciju između ponovnih rendera...
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // ...i dok god se ove zavisnosti ne menjaju...
return (
<div className={theme}>
{/* ...ShippingForm će primiti iste props-e i preskočiti ponovni render */}
<ShippingForm onSubmit={handleSubmit} />
</div>
);
}
Obmotavanjem handleSubmit
-a sa useCallback
osiguravate da je to ista funkcija između ponovnih rendera (dok se zavisnosti ne promene). Ne trebate da obmotate funkciju sa useCallback
ako nemate poseban razlog. U ovom primeru, razlog je to što je prosleđujete u komponentu obmotanu sa memo
, pa vam to omogućava da preskočite ponovno renderovanje. Postoje i drugi razlozi za upotrebu useCallback
-a koji su opisani kasnije na ovoj stranici.
Deep Dive
Često ćete videti useMemo
pored useCallback
-a. Oba su korisna kada pokušavate optimizovati dečju komponentu. Omogućavaju vam da memoizujete (ili, drugim rečima, keširate) nešto što prosleđujete deci:
import { useMemo, useCallback } from 'react';
function ProductPage({ productId, referrer }) {
const product = useData('/product/' + productId);
const requirements = useMemo(() => { // Poziva vašu funkciju i kešira rezultat
return computeRequirements(product);
}, [product]);
const handleSubmit = useCallback((orderDetails) => { // Kešira samu funkciju
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]);
return (
<div className={theme}>
<ShippingForm requirements={requirements} onSubmit={handleSubmit} />
</div>
);
}
Razlika je u tome šta vam omogućavaju da keširate:
useMemo
kešira rezultat poziva vaše funkcije. U ovom primeru, kešira rezultat pozivacomputeRequirements(product)
tako da se ne menja dok seproduct
ne promeni. Ovo vam omogućava da proslediterequirements
objekat bez nepotrebnih ponovnih renderaShippingForm
-a. Kada je neophodno, React će tokom renderovanja pozvati funkciju koju ste prosledili kako bi izračunao rezultat.useCallback
kešira samu funkciju. Za razliku oduseMemo
-a, ne poziva funkciju koju prosledite. Umesto toga, kešira prosleđenu funkciju tako da se samahandleSubmit
ne menja osim ako seproductId
ilireferrer
promene. Ovo vam omogućava da prosleditehandleSubmit
funkciju bez nepotrebnih ponovnih renderaShippingForm
-a. Vaš kod se neće izvršiti dok korisnik ne submit-uje formu.
Ako ste već upoznati sa useMemo
, može vam biti lakše da razmišljate o useCallback
-u na ovaj način:
// Pojednostavljena implementacija (unutar React-a)
function useCallback(fn, dependencies) {
return useMemo(() => fn, dependencies);
}
Deep Dive
Ako je vaša aplikacija poput ovog sajta, gde su većinom grube interakcije (poput zamene stranice ili cele sekcije), memoizacija uglavnom nije potrebna. Na drugu stranu, ako vam aplikacija liči na editor crteža i interakcije su uglavnom granularne (poput pomeranja oblika), onda vam memoizacija može biti od velike pomoći.
Keširanje funkcije sa useCallback
je korisno samo u par slučajeva:
- Prosleđujete je kao prop u komponentu koja je obmotana sa
memo
. Želite preskočiti ponovno renderovanje ako se vrednost nije promenila. Memoizacija čini da se vaša komponenta ponovo renderuje samo ako se zavisnosti promene. - Funkcija koju prosleđujete se kasnije koristi kao zavisnost nekog Hook-a. Na primer, druga funkcija obmotana sa
useCallback
zavisi od nje, ili zavisite od te funkcije krozuseEffect
.
Nema benefita obmotavati funkciju sa useCallback
u ostalim slučajevima. Doduše, ne postoji ni značajna šteta u tome, pa neki timovi odlučuju da ne razmišljaju o pojedinačnim slučajevima i da memoizuju što je više moguće. Loša strana je da kod postaje manje čitljiv. Takođe, nije svaka memoizacija efikasna: pojedinačna vrednost koja je “uvek nova” je dovoljna da slomi memoizaciju za celu komponentu.
Primetite da useCallback
ne sprečava kreiranje funkcije. Uvek kreirate funkciju (i to je u redu!), ali React to ignoriše i vraća vam keširanu funkciju ako se ništa nije promenilo.
U praksi dosta memoizacije možete učiniti nepotrebnom ako pratite par principa:
- Kada komponenta vizuelno obmotava druge komponente, napravite da prima JSX kao decu. U tom slučaju, ako obmotavajuća komponenta ažurira svoj state, React zna da njena deca ne trebaju ponovo da se renderuju.
- Koristite lokalni state i nemojte podizati state više nego što je potrebno. Nemojte čuvati prolazni state poput formi i podataka da li prelazite mišem preko nečega na vrhu stabla ili u globalnoj state biblioteci.
- Postarajte se da je logika renderovanja čista. Ako ponovni render komponente pravi problem ili neki uočljivi vizuelni artefakt, to je bug u komponenti! Popravite bug umesto dodavanja memoizacije.
- Izbegavajte nepotrebne Effect-e koji ažuriraju state. Većina problema sa performansama u React aplikacijama prouzrokovani su nizom ažuriranja koji potiču od Effect-a koji iznova i iznova renderuju vaše komponente.
- Pokušajte da uklonite nepotrebne zavisnosti u Effect-ima. Na primer, umesto memoizacije, često je lakše pomeriti neki objekat ili funkciju unutar Effect-a ili izvan komponente.
Ako neka posebna interakcija i dalje deluje da lag-uje, iskoristite React Developer Tools profiler da vidite koje komponente mogu imati benefita od memoizacije i dodajte memoizaciju gde je potrebno. Ovi principi čine vaše komponente lakšim za debug-ovanje i razumevanje, pa je dobro da ih uvek pratite. Dugoročno, mi istražujemo automatsku upotrebu memoizacije da ovo rešimo jednom za svagda.
Primer 1 od 2: Preskakanje ponovnog renderovanja sa useCallback
i memo
U ovom primeru, ShippingForm
komponenta je veštački usporena kako biste mogli videti šta se dešava kada je React komponenta koju renderujete zapravo spora. Pokušajte inkrementirati brojač i menjati temu.
Inkrementiranje brojača deluje sporo jer tera usporenu ShippingForm
komponentu da se ponovo renderuje. To je očekivano jer se brojač promenio, pa morate da prikažete korisnikov novi izbor na ekranu.
Posle toga probajte da promenite temu. Zahvaljujući useCallback
-u zajedno sa memo
, promena je brza uprkos veštačkom usporavanju! ShippingForm
je preskočila ponovno renderovanje jer se handleSubmit
funkcija nije promenila. handleSubmit
funkcija se nije promenila jer se ni productId
ni referrer
(zavisnosti useCallback
-a) nisu promenili od poslednjeg rendera.
import { useCallback } from 'react'; import ShippingForm from './ShippingForm.js'; export default function ProductPage({ productId, referrer, theme }) { const handleSubmit = useCallback((orderDetails) => { post('/product/' + productId + '/buy', { referrer, orderDetails, }); }, [productId, referrer]); return ( <div className={theme}> <ShippingForm onSubmit={handleSubmit} /> </div> ); } function post(url, data) { // Zamisli slanje zahteva... console.log('POST /' + url); console.log(data); }
Ažuriranje state-a iz memoizovanog callback-a
Ponekad će vam trebati da ažurirate state na osnovu prethodnog state-a u memoizovanom callback-u.
Ova handleAddTodo
funkcija specificira todos
kao zavisnost jer na osnovu nje računa naredni todos:
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos([...todos, newTodo]);
}, [todos]);
// ...
Uglavnom želite da memoizovane funkcije imaju što manje zavisnosti. Kada neki state čitate samo da biste izračunali naredni state, možete ukloniti tu zavisnost prosleđivanjem updater funkcije:
function TodoList() {
const [todos, setTodos] = useState([]);
const handleAddTodo = useCallback((text) => {
const newTodo = { id: nextId++, text };
setTodos(todos => [...todos, newTodo]);
}, []); // ✅ Nema potrebe za todos zavisnošću
// ...
Ovde, umesto da imate todos
zavisnost i da je čitate, React-u prosleđujete instrukciju kako ažurirati state (todos => [...todos, newTodo]
). Pročitajte još o updater funkcijama.
Sprečavanje prečestog okidanja Effect-a
Ponekad vam može biti potrebno da pozovete funkciju unutar Effect-a:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
// ...
Ovo stvara problem. Svaka reaktivna vrednost mora biti deklarisana kao zavisnost vašeg Effect-a. Međutim, ako deklarišete createOptions
kao zavisnost, to će uticati da se vaš Effect konstantno rekonektuje na sobu za dopisivanje:
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🔴 Problem: Ova zavisnost se menja svakim renderom
// ...
Da biste ovo rešili, možete obmotati funkciju koju trebate pozvati unutar Effect-a sa useCallback
:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const createOptions = useCallback(() => {
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}, [roomId]); // ✅ Menja se samo kada se roomId promeni
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // ✅ Menja se samo kada se createOptions promeni
// ...
Ovo osigurava da je createOptions
funkcija ista između ponovnih rendera ako je roomId
isto. Međutim, još je bolje ukloniti potrebu da imate funkciju kao zavisnost. Pomerite funkciju unutar Effect-a:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
useEffect(() => {
function createOptions() { // ✅ Nema potrebe za useCallback ili funkcijama kao zavisnostima!
return {
serverUrl: 'https://localhost:1234',
roomId: roomId
};
}
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Menja se samo kada se roomId promeni
// ...
Sada je vaš kod jednostavniji i nema potrebu za useCallback
. Naučite više o uklanjanju Effect zavisnosti.
Optimizovanje prilagođenog Hook-a
Ako pišete prilagođeni Hook, preporučujemo da sve funkcije koje on vraća obmotate sa useCallback
:
function useRouter() {
const { dispatch } = useContext(RouterStateContext);
const navigate = useCallback((url) => {
dispatch({ type: 'navigate', url });
}, [dispatch]);
const goBack = useCallback(() => {
dispatch({ type: 'back' });
}, [dispatch]);
return {
navigate,
goBack,
};
}
Ovo osigurava da korisnici vašeg Hook-a mogu optimizovati svoj kod kada bude potrebno.
Rešavanje problema
Svaki put kad se moja komponenta renderuje, useCallback
vraća drugačiju funkciju
Postarajte se da specificirate niz zavisnosti kao drugi argument!
Ako zaboravite niz zavisnosti, useCallback
će vratiti novu funkciju svaki put:
function ProductPage({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}); // 🔴 Vraća novu funkciju svaki put: nema niza zavisnosti
// ...
Ovo je ispravljena verzija gde se prosleđuje niz zavisnosti kao drugi argument:
function ProductPage({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
}, [productId, referrer]); // ✅ Ne vraća novu funkciju bespotrebno
// ...
Ako vam ovo ne pomaže, problem je u tome da je barem jedna od vaših zavisnosti različita od prethodnog rendera. Možete debug-ovati ovaj problem ručnim logovanjem vaših zavisnosti u konzolu:
const handleSubmit = useCallback((orderDetails) => {
// ..
}, [productId, referrer]);
console.log([productId, referrer]);
Zatim možete upotrebiti desni klik na nizove iz različitih rendera u konzoli i izabrati “Store as a global variable” za oba. Pretpostavkom da je prvi sačuvan kao temp1
, a drugi kao temp2
, možete upotrebiti konzolu pretraživača da proverite da li je svaka od zavisnosti u oba niza jednaka:
Object.is(temp1[0], temp2[0]); // Da li je prva zavisnost jednaka u oba niza?
Object.is(temp1[1], temp2[1]); // Da li je druga zavisnost jednaka u oba niza?
Object.is(temp1[2], temp2[2]); // ... i tako dalje za svaku zavisnost ...
Kada pronađete koja zavisnost kvari memoizaciju, pronađite način da je uklonite ili je takođe memoizujte.
Trebam pozvati useCallback
za svaki član niza u petlji, ali nije dozvoljeno
Pretpostavimo da je Chart
komponenta obmotana sa memo
. Želite da preskočite ponovno renderovanje svakog Chart
-a u listi kada se ReportList
komponenta ponovo renderuje. Međutim, ne možete pozvati useCallback
u petlji:
function ReportList({ items }) {
return (
<article>
{items.map(item => {
// 🔴 Ne možeš pozvati useCallback u petlji ovako:
const handleClick = useCallback(() => {
sendReport(item)
}, [item]);
return (
<figure key={item.id}>
<Chart onClick={handleClick} />
</figure>
);
})}
</article>
);
}
Umesto toga, izdvojite komponentu za posebnu stavku i tamo stavite useCallback
:
function ReportList({ items }) {
return (
<article>
{items.map(item =>
<Report key={item.id} item={item} />
)}
</article>
);
}
function Report({ item }) {
// ✅ Pozovi useCallback na vrhu komponente:
const handleClick = useCallback(() => {
sendReport(item)
}, [item]);
return (
<figure>
<Chart onClick={handleClick} />
</figure>
);
}
Alternativno, možete ukloniti useCallback
iz poslednjeg snippet-a i umesto toga obmotati Report
sa memo
. Ako se item
prop ne promeni, Report
će preskočiti ponovni render, što znači da će i Chart
preskočiti ponovni render:
function ReportList({ items }) {
// ...
}
const Report = memo(function Report({ item }) {
function handleClick() {
sendReport(item);
}
return (
<figure>
<Chart onClick={handleClick} />
</figure>
);
});