Gli eventi in React | Aulab

GUIDE PER ASPIRANTI PROGRAMMATORI

Gli eventi in React

Cos è un evento in React? Abbiamo visto come passare le informazioni dai componenti genitori ai componenti figli, tramite props, ma come possiamo fare il contrario, cioè passare informazioni dai figli ai genitori? La risposta potrebbe soprenderti: non lo facciamo. In React, le informazioni viaggiano solo in un senso, dai componenti genitori ai componenti figli.…

Lezione 12 / 41
Enza Neri
Immagine di copertina

Vuoi avviare una nuova carriera o fare un upgrade?

Trova il corso Digital & Tech più adatto a te nel nostro catalogo!

Cos è un evento in React?

Abbiamo visto come passare le informazioni dai componenti genitori ai componenti figli, tramite props, ma come possiamo fare il contrario, cioè passare informazioni dai figli ai genitori?

La risposta potrebbe soprenderti: non lo facciamo. In React, le informazioni viaggiano solo in un senso, dai componenti genitori ai componenti figli.

Le informazioni in React viaggiano in una sola direzione e puoi decidere tu come immaginare questa direzione (verso l’alto o verso il basso, in orizzontale, in diagonale o a spirale). In questa sede immaginiamo le informazioni viaggiare dall’alto verso il basso, perché visualizziamo i componenti come fossero distribuiti come un diagramma di flusso, ma tu potresti visualizzarli come un albero, con le radici in basso e le informazioni che viaggiano verso l’alto: non ha alcuna importanza. L’importante è che, presa una direzione, le informazioni viaggino sempre e solo in quella direzione, che è dai componenti genitori ai componenti figli.

Il compito dei genitori è quello di inviare informazioni ai figli, quello dei figli è di notificare i genitori in caso di cambiamenti. Queste “notifiche” si inviano tramite eventi.

Gli eventi che accadono più spesso sono quelli che arrivano dal DOM (Document Object Model, cioè l’insieme degli elementi HTML). Ogni movimento del mouse, click o tocco sullo schermo, tasto premuto o rilasciato dalla tastiera, genera un evento: è compito della nostra applicazione decidere quali sono importanti per il suo funzionamento e quali non lo sono.

Quando decidiamo che un evento ci interessa, decidiamo di ascoltarlo, collegando un event listener (ascoltatore di eventi) all’elemento HTML e all’evento che ci interessa. Ogni listener è collegato a una coppia di un evento e un elemento HTML.

Per esempio, potremmo essere interessati al click di un bottone che recita “Click me!”. Non siamo interessati a tutti i click, né a tutti i bottoni, solo ai click su quel particolare bottone, cioè alla coppia bottone “Click me!” ed evento click.

Modifichiamo, quindi, Message per mostrare un bottone. Manteniamo la proprietà message, che diventa l’etichetta del bottone.

// File: src/components/Message.jsx

export default function Message({ message }) {
  return <button className="Message">{message}</button>;
}
Ora aggiungiamo un event listener al click del bottone:
// File: src/components/Message.jsx

export default function Message({ message }) {
  return (
    <button
      className="Message"
      onClick={() => {
        console.log("The button was clicked!");
      }}
    >
      {message}
    </button>
  );
}

Nota: se hai dimestichezza con la programmazione a oggetti, potrebbe aiutarti immaginare il bottone come un oggetto e l’attributo onClick come un metodo della classe da cui il bottone è istanziato. La coppia elemento-evento a cui ci interessiamo corrisponde a una coppia oggetto-metodo.

the button was clicked!

Come abbiamo fatto più volte in precedenza, possiamo inserire il nostro event listener in una costante e iniettarla nella parte di JSX:

// File: src/components/Message.jsx

export default function Message({ message }) {
  const onClick = () => {
    console.log("The button was clicked!");
  };

  return (
    <button className="Message" onClick={onClick}>
      {message}
    </button>
  );
}

Possiamo notare come stiamo passando una funzione come attributo a un elemento HTML. Abbiamo accennato in precedenza a come in React sia perfettamente normale avere attributi che non sono soltanto stringhe, ma qualsiasi tipo di valore valido in JavaScript.

Ora che il nostro componente Message può intercettare il click del bottone, possiamo avvisare di questo evento anche App, utilizzando lo stesso metodo. Questa volta, però, dovremo aggiungere una prop:

// File: src/components/Message.jsx

export default function Message({ message, onButtonClick }) {
  const onClick = () => {
    onButtonClick();
  };

  return (
    <button className="Message" onClick={onClick}>
      {message}
    </button>
  );
}

Con il cambiamento soprastante, dichiariamo che il componente Message si aspetta una prop, onButtonClick, che altro non è che un event listener, esattamente come onClick dell’elemento HTML button. Quando il bottone viene cliccato, la funzione onClick lo “fa rimbalzare” al componente parente di Message. In altre parole, button avvisa Message e Message avvisa il suo componente genitore, che in questo caso è App:

// File: src/App.jsx

import "./App.css";
import Message from "./components/Message";
import Panel from "./components/Panel";

export default function App() {
  const message = "Look, a message!";

  const onButtonClick = () => {
    console.log("The button was clicked!");
  };

  return (
    <div className="App">
      <Panel>
        <Message message={message} onButtonClick={onButtonClick} />
      </Panel>
    </div>
  );
}

Abbiamo spostato la gestione del click dal componente figlio al componente genitore.

Eventi in React VS eventi in Javascript

Il metodo con cui assegnamo event listener in React assomiglia molto a quello nativo JavaScript:

// Esempio di un event listener nativo JavaScript
const buttonElement = document.querySelector("button");

buttonElement.addEventListener("click", () => {
  console.log("The button was clicked!");
});

Ci sono, però, un paio di differenze:

  1. In JSX, avendo direttamente accesso all’elemento HTML, possiamo collegare il nostro event listener direttamente ad esso. Un metodo nativo molto simile è quello di utilizzare l’attributo onclick che, però, implica l’uso di una funzione globale (i.e.: legata a window) che è sempre una scommessa da un punto di vista di sicurezza (a seconda di cosa fa la funzione listener). Nel caso di React, la funzione listener è all’interno del componente (in uno scope isolato) per cui non abbiamo problemi di sicurezza. Come da prassi quando si tratta di React, utilizziamo una forma camelCase (onClick).
  2. Così come in linguaggio JavaScript nativo, alla funzione listener viene passato un argomento, che rappresenta l’evento. A differenza di JavaScript nativo, però, l’evento passa attraverso React, che lo trasforma in un evento sintetico (nella versione TypeScript di React si parla proprio di SyntheticEvent). Questo è uguale a un evento nativo per certi aspetti (preventDefault e stopPropagation, per esempio, funzionano nello stesso modo), mentre porta alcune differenze, le più importanti delle quali saranno affrontate nelle prossime sezioni.

Passare una funzione VS chiamare una funzione

React riserva un posto speciale per le funzioni, come possiamo notare dal fatto che i componenti stessi, che sono alla base del suo funzionamento, sono funzioni. Le funzioni sono i verbi del programmatore e nello stesso modo in cui, nel linguaggio naturale, le cose possono diventare complesse, così è anche in programmazione informatica, soprattutto nella programmazione dichiarativa.

Parlando di eventi, allora, affrontiamo una delle differenze fondamentali e più difficili da afferrare per chi si affaccia al mondo della programmazione funzionale: quella tra il passaggio di una funzione e l’esecuzione di una funzione. Questa differenza ha generato così tanta confusione che Vue, un altro framework front end incredibilmente popolare, l’ha eliminata completamente.

Partiamo dalle cose semplici: immaginiamo di voler costruire un sistema che ci dice se una stringa è vuota o non vuota, che verrà collegato a un form e servirà a validare, per esempio, i campi “nome” e “cognome” durante la registrazione di un utente.

Cominciamo con una funzione che, data una stringa, restituisce true se la stringa non è vuota, false se è vuota. Possiamo chiamarla isNonEmptyString:

function isNonEmptyString(string) {
  return typeof string === "string" && string !== "";
}

// Esempi di utilizzo
console.log(isNonEmptyString("Anything")); // true
console.log(isNonEmptyString("")); // false

Nota: dato che in questo caso le stringhe vuote sono un errore, ha senso restituire true per le stringhe non vuote e non il contrario. Inoltre, abbiamo aggiunto un controllo sul tipo così che qualsiasi cosa non sia una stringa venga trattata come una stringa vuota che, in questo caso, indica un errore.

Ora, per fare un validatore servono due cose: la funzione di validazione e il messaggio da mostrare in caso di errore. Potremmo pensare di creare una nuova funzione che usa isNonEmptyString in un modo simile a questo:

function getEmptyStringError(string, errorMessage) {
  const validationResult = isNonEmptyString(string);

  if (validationResult) {
    return null;
  } else {
    return errorMessage;
  }
}

// Esempi di utilizzo
const nonEmptyStringError = getEmptyStringError("Anything", "Error!");
const emptyStringError = getEmptyStringError("", "Error!");

console.log(nonEmptyStringError); // null
console.log(emptyStringError); // "Error!"

In caso di stringa vuota, getEmptyStringError restituirà il messaggio di errore, altrimenti restituirà null (i.e.: nessun errore). Passiamo il messaggio di errore come argomento per metterci in condizione di chiamare la funzione più volte con errori diversi per campi diversi. Per esempio, nome e cognome in un form avrebbero messaggi errori diversi:

const firstNameValidation = getEmptyStringError(
  firstName,
  "Your first name cannot be empty!"
);

const lastNameValidation = getEmptyStringError(
  lastName,
  "Your last name cannot be empty!"
);

Ora, decidiamo di fare la stessa cosa per validare la maggiore età di un utente, con una funzione che chiede gli anni d’età e verifica che sia almeno 18:

function isOfAge(ageInYears) {
  return ageInYears >= 18;
}

function getNotOfAgeError(ageInYears, errorMessage) {
  const validationResult = isOfAge(ageInYears);

  if (validationResult) {
    return null;
  } else {
    return errorMessage;
  }
}

// Esempi di utilizzo
const ofAgeError = getNotOfAgeError(18, "Error!");
const notOfAgeError = getNotOfAgeError(17, "Error!");

console.log(ofAgeError); // null
console.log(notOfAgeError); // "Error!"
Noti qualcosa? Entrambe le funzioni hanno la stessa forma, che potremmo rappresentare così:
function getValidationError(input, errorMessage) {
  const validationResult = validation(input);

  if (validationResult) {
    return null;
  } else {
    return errorMessage;
  }
}

Con un occhio al futuro, immaginando che i validatori saranno molteplici, decidiamo di spezzare la logica. Per farlo, ci servono input, validation e errorMessage. Proviamo a creare una funzione che li prende tutti come argomenti:

function getValidationError(validation, input, errorMessage) {
  const validationResult = validation(input);

  if (validationResult) {
    return null;
  } else {
    return errorMessage;
  }
}

function isNonEmptyString(string) {
  return typeof string === "string" && string !== "";
}

function isOfAge(ageInYears) {
  return ageInYears >= 18;
}

Notiamo come validation è usato come una funzione (validation(input)). Per usare la nuova funzione getValidationError, dobbiamo trattare isOfAge e isNonEmptyString come valori e non come funzioni!

// Esempi d'uso
const nonEmptyStringError = getValidationError(
  isNonEmptyString,
  "Anything",
  "Error!"
);
const emptyStringError = getValidationError(isNonEmptyString, "", "Error!");

const ofAgeError = getValidationError(isOfAge, 18, "Error!");
const notOfAgeError = getValidationError(isOfAge, 17, "Error!");

console.log(nonEmptyStringError); // null
console.log(emptyStringError); // "Error!"
console.log(ofAgeError); // null
console.log(notOfAgeError); // "Error!"

La differenza sostanziale sta tra:

// Prima
const validationResult = isNonEmptyString(string);
//                       ^^^^^^^^^^^^^^^^

// Dopo
const emptyStringError = getValidationError(isNonEmptyString, "", "Error!");
//

Nel primo caso, chiamiamo la funzione isNonEmptyString, nel secondo la passiamo. La differenza sintattica sono le parentesi dopo il nome della funzione: (string); la differenza concettuale è che nel secondo caso ci penserà la funzione getValidationError a chiamare isNonEmptyString.

Sei indeciso sul percorso? 💭

Parliamone! Scrivici su Whatsapp e risponderemo a tutte le tue domande per capire quale dei nostri corsi è il più adatto alle tue esigenze.

Oppure chiamaci al 800 128 626