30 Ιουν, 2022
React: useMemo vs useCallback
Πρώτα από όλα ας εξηγήσουμε γρήγορα τι είναι το useMemo
και useCallback
στην React.
Και τα δυο είναι React hooks που έχουν να κάνουν με έναν όρο που λέγεται Memoization.
Σύμφωνα με τη Wikipedia:
Στην πληροφορική, memoization είναι μια τεχνική βελτιστοποίησης η οποία χρησιμοποιείται κυρίως για να επιταχύνει τα προγράμματα υπολογιστή αποθηκεύοντας τα αποτελέσματα από τις κλήσεις μιας κοστοβόρας συνάρτησης και επιστρέφοντας το αποθηκευμένο αποτέλεσμα όταν δέχεται τις ίδιες εισόδους.
Μια memoized συνάρτηση "θυμάται" τα αποτελέσματα που αντιστοιχούν σε συγκεκριμένες εισόδους. Ως αποτέλεσμα, διαδοχικές κλήσεις με παρελθοντικές εισόδους επιστρέφουν το ήδη αποθηκευμένο αποτέλεσμα αντί να χρειαστεί να ξανα υπολογιστεί.
Για παράδειγμα θεωρείστε μια κοστοβόρα συνάρτηση f(n)
που υπολογίζει το παραγοντικό του αριθμού n
. Ξέρουμε ότι το τελικό αποτέλεσμα αυτής της συνάρτησης είναι αμετάβλητο - π.χ το παραγοντικό του 3 θα είναι πάντα 6. Οπότε γιατί απλά να μην αποθηκεύουμε το αποτέλεσμα και να το επιστρέφουμε την επόμενη φορά που θα ξανα καλεστεί με είσοδο 3; Αυτό είναι memoization.
useMemo στη React
Υποθέστε ότι έχουνε ένα React component που χρειάζεται να τρέξει μια κοστοβόρα συνάρτηση που υπολογίζει μια τιμή. Οι παράμετροι αυτής της συνάρτησης παίρνονται από τα props του component - οπότε δε μπορούμε να την μετακινήσουμε έξω από το component.
Αυτό σημαίνει ότι η συνάρτηση θα καλείται σε κάθε re-render του component.
const Component = (props) => {
const {a, b, ...otherProps} = props;
const x = computeExpensiveValue(a, b);
return <div>{x}</div>
}
Μια τέτοια υλοποίηση θα ήταν καταστροφική για την απόδοση και το UI σίγουρα θα φαινόταν unresponsive.
🎉 Το useMemo hook είναι εδώ!
Με το useMemo
μπορούμε να αποθηκεύσουμε (memoize) το αποτέλεσμα της συνάρτησης computeExpensiveValue
. Αυτό σημαίνει ότι η συνάρτηση θα ξανα-τρέχει μόνο όταν οι παράμετροι a ή β αλλάζουν και όχι σε κάθε re-render.
Η σύνταξη είναι πολύ απλή, απλώς περνάμε ένα πίνακα από dependencies στο useMemo
hook. Το useMemo
θα ξανα-υπολογίσει την τιμή μόνο όταν κάποιο από τα dependencies αλλάζει.
Εάν δεν δοθεί κανένας πίνακας, τότε μια καινούργια τιμή θα υπολογίζεται σε κάθε render.
const Component = (props) => {
const {a, b, ...otherProps} = props;
const x = useMemo(() => computeExpensiveValue(a, b), [a, b]);
return <div>{x}</div>
}
Φυσικά, πρέπει να είμαστε προσεκτικοί με το useMemo και να το χρησιμοποιούμε μόνο σαν performance optimization, όπως δηλώνει και η React:
Μπορείτε να βασίζεστε στο useMemo μόνο σαν performance optimization και όχι σαν semantic guarantee. Στο μέλλον, η React ίσως επιλέξει να "ξεχνάει" παρελθοντικές memoized τιμές και να τις ξανα-υπολογίζει στο επόμενο render, π.χ για να απελευθερώσει μνήμη σε offscreen components. Γράψτε τον κώδικα σας ώστε να δουλέυει ακόμα και χωρίς useMemo - και μετά προσθέστε το για να βελτιώσετε την απόδοση.
useCallback στη React
Εντάξει, τώρα που καταλάβαμε το useMemo, ας μιλήσουμε λίγο και για το useCallback
hook.
Αυτό το hook είναι χρήσιμο εάν θέλουμε να κάνουμε memoize functions (callbacks).
Το πιο σύνηθες σενάριο που να θέλουμε να κάνουμε memoize μια function είναι όταν την περνάμε (την συνάρτηση) κάτω σε pure child components.
Για παράδειγμα δείτε το παρακάτω σενάριο:
const PureComponent = React.memo((props) => {
const {onToggleClick, ...otherProps} = props;
return <button onClick={() => onToggleClick()}>some button</button>
});
const Father = (props) => {
const {a, b} = props;
return <div>
<PureComponent {...otherProps} onToggleClick={() => console.log(a, b)} />
{/* ... other stuff here ...*/}
</div>
}
Θέλουμε το PureComponent
να κάνει re-render μόνο όταν αλλάζουν τα props του - για αυτό και το "τυλίξαμε" σε ένα React.memo
call. Το React.memo δουλεύει με το να κάνει shallowly compare στις αλλαγές των props.
Ωστόσο, όταν το Father
component κάνει re-render, η onToggleClick
συνάρτηση ξανα-δημιουργείται - δεν έχει σταθερή αναφορά (stable reference). Ως αποτέλεσμα, όλη η βελτιστοποίηση που κάναμε με το React.memo είναι άσκοπη.
🎉 To useCallback hook είναι εδώ!
Χρησιμοποιώντας το useCallback
hook, μπορούμε να δώσουμε stable reference στην συνάρτηση onToggleClick
.
const onToggleClick = useCallback(() => console.log(a, b), []);
Δεν έχουμε τελειώσει ακόμη. Ίσως παρατηρήσατε ένα πίνακα στην δεύτερη παράμετρο του useCallback
. Αυτός είναι ο dependencies array που θα υποδείξει στο useCallback
να αλλάζει όποτε ένα από τα dependencies αλλάζουν - με αυτό τον τρόπο η συνάρτηση onToggleClick
θα έχει πάντα πρόσβαση στις τελευταίες τιμές των a και b.
Όπως δηλώνει η React:
... κάθε τιμή που αναφέρεται μέσα στο callback πρέπει να εμφανίζεται και στο dependencies array ...
Άρα η τελική υλοποίηση θα είναι κάπως έτσι:
const PureComponent = React.memo((props) => {
const {onToggleClick, ...otherProps} = props;
return <button onClick={() => onToggleClick()}>some button</button>
})
const Father = (props) => {
const {a, b} = props;
const onToggleClick = useCallback(() => console.log(a, b), [a, b]);
return <div>
<PureComponent {...otherProps} onToggleClick={onToggleClick} />
{/* ... other stuff here ...*/}
</div>
}
Συνοψίζοντας:
useMemo
: Είναι για να κάνουμε memoize ένα calculation μεταξύ κλήσεων μιας συνάρτησης μεταξύ των renders.useCallback
: Είναι για να κάνουμε memoize ένα callback (referential equality) μεταξύ των renders.
Αξίζει επίσης να προσθέσουμε ότι το:
useCallback(fn, deps)
είναι ισοδύναμο με:
useMemo(() => fn, deps)
Τέλος
Ελπίζω να βρήκατε το άρθρο χρήσιμο.
Τα λέμε σύντομα! 🙂
Εγγραφή
Εγγραφειτε στην λιστα
Εγγραφείτε με το e-mail σας για να σας στέλνω το υλικό μου. Δεν θα είναι spam, σας το υπόσχομαι! Μπορείτε να καταργήσετε την εγγραφή σας όποτε θέλετε.