30 Ιουν, 2022

React: useMemo vs useCallback

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)

Τέλος

Ελπίζω να βρήκατε το άρθρο χρήσιμο.

Τα λέμε σύντομα! 🙂

Loading Comments...