Non è un segreto che React.js è diventato molto popolare negli ultimi anni. Ora è la libreria JavaScript scelta da molti degli attori più importanti di internet, compresi Facebook e WhatsApp.

Una delle ragioni principali della sua ascesa è stata l’introduzione degli hook a partire dalla versione 16.8. Gli hook permettono di attingere alle funzionalità di React senza scrivere componenti di classe. Ora i componenti funzionali con gli hook sono diventati la struttura preferita dagli sviluppatori che lavorano con React.

In questo articolo, analizzeremo approfonditamente uno specifico hook – useCallback – perché tocca una parte fondamentale della programmazione funzionale conosciuta come memoization. Saprete esattamente come e quando utilizzare l’hook useCallback e sfruttarlo al massimo per migliorare le prestazioni.

Siete pronti? Cominciamo!

Cos’è la Memoizzazione?

La memoizzazione è quando una funzione complessa memorizza il suo output in modo che la volta successiva venga chiamata con lo stesso input. È simile al caching, ma ad un livello più locale. Può saltare qualsiasi calcolo e restituire l’output più velocemente perché questo è già calcolato.

Questo può avere un effetto significativo sull’allocazione della memoria e sulle prestazioni, e questa tensione è ciò che l’hook useCallback ha lo scopo di alleviare.

React’s useCallback vs useMemo

A questo punto, vale la pena dire che useCallback si abbina bene con un altro hook chiamato useMemo. Li analizzeremo entrambi, ma in questo articolo ci concentreremo su useCallback.

La differenza principale è che useMemo restituisce un valore memoizzato, mentre useCallback restituisce una funzione memoizzata. Ciò significa che useMemo è utilizzato per memoizzare un valore calcolato, mentre useCallback restituisce una funzione che potete invocare in seguito.

Questi hook vi restituiranno una versione in cache a meno che una delle dipendenze (ad esempio lo stato o le proprietà) non cambi.

Diamo un’occhiata alle due funzioni in azione:

import { useMemo, useCallback } from 'react'
const values = [3, 9, 6, 4, 2, 1]

// Questo restituirà sempre lo stesso valore, un array ordinato. Quando l'array dei valori cambia, viene ricalcolato.
const memoizedValue = useMemo(() => values.sort(), [values])

// Questo mi restituirà una funzione che potrà essere richiamata in seguito. Restituirà sempre lo stesso risultato, a meno che l'array dei valori non venga modificato.
const memoizedFunction = useCallback(() => values.sort(), [values])

Lo snippet di codice qui sopra è un esempio artificioso ma mostra la differenza tra le due callback:

  1. memoizedValue diventerà l’array [1, 2, 3, 4, 6, 9]. Finché la variabile dei valori rimane, lo farà anche memoizedValue e non si ricompatterà mai.
  2. memoizedFunction sarà una funzione che restituirà l’array [1, 2, 3, 4, 6, 9].

Il bello di queste due callback è che vengono memoizzate nella cache e rimangono in giro fino a quando l’array di dipendenze non cambia. Questo significa che durante un rendering, non saranno garbage collected.

Rendering e React

Perché la memoizzazione è importante quando si parla di React?

La memoizzazione ha a che fare con il modo in cui React rende i vostri componenti. React utilizza un Virtual DOM archiviato in memoria per confrontare i dati e decidere cosa aggiornare.

Il DOM virtuale aiuta React nelle prestazioni e mantiene veloce la vostra applicazione. Di default, se un qualsiasi valore cambia nel vostro componente, l’intero componente viene reso nuovamente. Questo rende React “reattivo” all’input dell’utente e permette di aggiornare lo schermo senza ricaricare la pagina.

Non dovreste fare il rendering del vostro componente perché le modifiche non avranno effetto su quel componente. È qui che diventa utile la memoizzazione attraverso useCallback e useMemo.

Quando React ri-renderizza il vostro componente, ricrea anche le funzioni che avete dichiarato all’interno del vostro componente.

Si noti che, quando si confronta l’uguaglianza di una funzione con un’altra funzione, sarà sempre falsa. Dato che una funzione è anche un oggetto, sarà uguale solo a se stessa:

// queste variabili contengono la stessa identica funzione, ma non sono uguali
const hello = () => console.log('Hello Matt')
const hello2 = () => console.log('Hello Matt')

hello === hello2 // false
hello === hello // true

In altre parole, quando React ri-renderizza il componente, vedrà tutte le funzioni dichiarate nel componente come nuove funzioni.

Questo va bene nella maggior parte dei casi, le funzioni sono semplici da calcolare e non avranno un impatto sulle prestazioni. Ma le volte in cui non volete che la funzione sia vista come una nuova funzione, potete contare su useCallback.

Potreste pensare: “Quando non voglio che una funzione sia vista come una nuova funzione? Beh, ci sono alcuni casi in cui useCallback ha più senso:

  1. State passando la funzione ad un altro componente anche questo memoizzato (useMemo)
  2. La vostra funzione ha uno stato interno che deve ricordare
  3. La vostra funzione è una dipendenza di un altro hook, come useEffect

Benefici delle Prestazioni di React useCallback

Se useCallback è usato in modo appropriato, può aiutarvi a rendere più veloce la vostra applicazione e ad evitare che i componenti vengano renderizzati nuovamente se non è necessario.

Supponiamo, ad esempio, che avete un componente che recupera una grande quantità di dati ed è responsabile della visualizzazione di quei dati sotto forma di un grafico o un diagramma, come questo:

Un grafico a barre colorato che confronta il tempo di transazione complessivo di PHP, MySQL, Reddis ed esterno (altro) in millisecondi..
Grafico a barre generato utilizzando un componente React.

Supponiamo che il componente genitore della vostra visualizzazione dei dati sia renderizzato nuovamente, ma le proprietà o lo stato cambiati non abbiano effetto su quel componente. In questo caso, probabilmente non si dovrebbe o non si ha bisogno di fare un nuovo rendering e recuperare tutti i dati. Evitare questo re-render e refetch può far risparmiare larghezza di banda al vostro utente e offrire una user experience più fluida.

Svantaggi di React useCallback

Anche se questo hook può aiutarvi a migliorare le prestazioni, presenta anche le sue insidie. Alcune cose da considerare prima di usare useCallback (e useMemo) sono:

  • Garbage collection: Le altre funzioni che non sono già memoizzate saranno buttate via da React per liberare memoria.
  • Allocazione della memoria: Simile alla garbage collection, più funzioni memoizzate avete, più memoria sarà richiesta. Inoltre, ogni volta che usate queste callback, c’è un sacco di codice all’interno di React che ha bisogno di ancora più memoria per fornirvi l’output della cache.
  • Complessità del codice: Quando iniziate ad avvolgere le funzioni in questi hook, aumentate immediatamente la complessità del codice. Ora è necessaria una maggiore comprensione del perché questi hook vengono utilizzati e la conferma che vengano utilizzati correttamente.

Essere consapevoli delle insidie di cui sopra può risparmiarvi dei grattacapi. Se state pensando di utilizzare useCallback, assicuratevi che i vantaggi in termini di prestazioni superino gli svantaggi.

Esempio di React useCallback

Qui sotto c’è una semplice configurazione con un componente Button e un componente Counter. Il contatore ha due parti di stato e rende due componenti Button, ognuno dei quali aggiornerà una parte separata dello stato del componente Counter.

Il componente Button riceve due proprietà: handleClick e name. Ogni volta che il pulsante viene reso a video, viene registrato nella console.

import { useCallback, useState } from 'react'

const Button = ({handleClick, name}) => {
  console.log(`${name} rendered`)
  return <button onClick={handleClick}>{name}</button>
}

const Counter = () => {

console.log('counter rendered')
  const [countOne, setCountOne] = useState(0)
  const [countTwo, setCountTwo] = useState(0)
  return (
    <>
      {countOne} {countTwo}
      <Button handleClick={() => setCountOne(countOne + 1)} name="button1" />
      <Button handleClick={() => setCountTwo(countTwo + 1)} name="button1" />
    </>
  )
}

In questo esempio, ogni volta che fate clic su uno dei due pulsanti, vedrete questo nella console:

// counter rendered

// button1 rendered
// button2 rendered

Ora, se applichiamo useCallback alle nostre funzioni handleClick e racchiudiamo il nostro pulsante in React.memo, possiamo vedere cosa ci fornisce useCallback. React.memo è simile a useMemo e permette di memoizzare un componente.

import { useCallback, useState } from 'react'

const Button = React.memo(({handleClick, name}) => {
  console.log(`${name} rendered`)
  return <button onClick={handleClick}>{name}</button>
})

const Counter = () => {
  console.log('counter rendered')
  const [countOne, setCountOne] = useState(0)
  const [countTwo, setCountTwo] = useState(0)
  const memoizedSetCountOne = useCallback(() => setCountOne(countOne + 1), [countOne)
  const memoizedSetCountTwo = useCallback(() => setCountTwo(countTwo + 1), [countTwo])
  return (
    <>
        {countOne} {countTwo}
        <Button handleClick={memoizedSetCountOne} name="button1" />
        <Button handleClick={memoizedSetCountTwo} name="button1" />
    </>
  )
}

Ora, quando clicchiamo su uno dei pulsanti, vedremo solo il pulsante su cui abbiamo cliccato per accedere alla console:

// counter rendered

// button1 rendered

// counter rendered

// button2 rendered

Abbiamo applicato la memoizzazione al nostro componente button e i valori prop che gli vengono passati sono visti come uguali. Le due funzioni handleClick sono memorizzate nella cache e saranno viste come la stessa funzione da React fino a quando il valore di un elemento nell’array di dipendenze non cambia (ad esempio countOne, countTwo).

Riepilogo

Per quanto useCallback e useMemo siano fantastici, ricordate che hanno casi d’uso specifici – non dovreste racchiudere ogni funzione con questi hook. Se la funzione è computazionalmente complessa, una dipendenza da un altro hook o una prop passata ad un componente memoizzato sono buoni motivi per aggiungere useCallback.

Ci auguriamo che questo articolo vi abbia aiutato a comprendere questa funzionalità avanzata di React e vi abbia aiutato a prendere maggiore confidenza con la programmazione funzionale!

Matthew Sobieray

Matthew works for Kinsta as a Development Team Lead from his home office in Denver, Colorado. He loves to learn, especially when it comes to web development.