Rendering condizionale in React | Aulab

GUIDE PER ASPIRANTI PROGRAMMATORI

Rendering condizionale in React

Abbiamo creato una funzione hook, useQuery, che ci permette di gestire delle richieste di rete, facendole partire non appena il componente che usa useQuery viene chiamato per la prima volta.  Possiamo pensare di creare un componente che rappresenti, tramite interfaccia grafica, una richiesta di rete. Questo ci permetterà di gestire nello stesso modo tutte le…

Lezione 28 / 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!

Abbiamo creato una funzione hook, useQuery, che ci permette di gestire delle richieste di rete, facendole partire non appena il componente che usa useQuery viene chiamato per la prima volta. 

Possiamo pensare di creare un componente che rappresenti, tramite interfaccia grafica, una richiesta di rete. Questo ci permetterà di gestire nello stesso modo tutte le parti di applicazione che dipendono da dati che arrivano dalla rete, rendendo incredibilmente più veloce lo sviluppo e, allo stesso tempo, dando coerenza al comportamento dell’interfaccia grafica.

Utilizziamo App per ospitare questo nuovo componente, immaginando che esista una lista di cose da fare già salvata dall’utente nelle sessioni precedenti. Questa è anche un’ottima occasione per parlare di rendering condizionale.

Il rendering condizionale è la tecnica che permette di ottenere elementi o componenti diversi a seconda dello stato dell’applicazione. Cominciamo con un esempio semplice.

If-else in React

// File: src/components/Query.jsx

import { useQuery } from "../hooks/useQuery";

export default function Query({ path, initialData }) {
  const [networkRequestState, retry] = useQuery({ path, initialData });

  if (networkRequestState.isIdle()) {
    return null;
  } else if (networkRequestState.isLoading()) {
    return <p style={{ textAlign: "center" }}>Loading...</p>;
  } else if (networkRequestState.isFailure()) {
    return (
      <p style={{ textAlign: "center", color: "red" }}>
        Error! {networkRequestState.error.message}
        <button onClick={retry}>Retry</button>
      </p>
    );
  } else if (networkRequestState.isSuccessful()) {
    return <p style={{ textAlign: "center" }}>Success!</p>;
  } else {
    return null;
  }
}

Il nuovo componente Query non è altro che una rappresentazione grafica di useQuery. Accetta gli stessi argomenti (che in questo caso sono props), che vengono passati tali e quali a useQuery. Per ogni stato possibile della macchina a stati finiti che rappresenta la richiesta di rete, c’è una vista grafica diversa.

Penseremo dopo al fatto che nel caso di stato “successful” non possiamo visualizzare un paragrafo con scritto “Success!” perché l’utente non saprebbe cosa farsene. Per adesso, concentriamoci sui vari modi di fare rendering condizionale.

Il primo modo di fare rendering condizionale è utilizzando if-else (o come in questo caso, if/else-if/else). I blocchi condizionali (if-else, appunto), sono nati per questo motivo. La programmazione imperativa ha portato una serie di varianti, come la programmazione difensiva, per esempio:

if (somethingWrong) {
  return;
}

goAhead();
ma i blocchi condizionali sono stati creati per dividere in due il flusso del codice:
if (something) {
  goOneDirection();
} else {
  goAnotherDirection();
}

In programmazione dichiarativa si tende a utilizzare if-else solo quando le strade che si possono prendere sono solo due, cioè tipicamente quando si tratta di valori booleani, come nell’ultimo esempio. Quando le opzioni possibili sono più di una, ha più senso utilizzare uno switch-case.

Switch-case in React

Non possiamo utilizzare uno switch-case con NetworkRequest, perché siamo andati troppo avanti e abbiamo isolato la gestione delle richieste di rete nella classe. Possiamo, però, fingere di non averlo mai fatto e di avere ancora a disposizione uno stato networkRequest che ha una proprietà type e un oggetto NetworkRequestType con i possibili valori di type:

/*
export const NetworkRequestType = {
  idle: "idle",
  loading: "loading",
  success: "success",
  failure: "failure",
};
*/

import { useQuery } from "../hooks/useQuery";

export default function Query({ path, initialData }) {
  const [networkRequestState, retry] = useQuery({ path, initialData });

  switch (networkRequestState.type) {
    case NetworkRequestType.idle:
      return null;
    case NetworkRequestType.loading:
      return <p style={{ textAlign: "center" }}>Loading...</p>;
    case NetworkRequestType.success:
      return (
        <p style={{ textAlign: "center", color: "red" }}>
          Error! {networkRequestState.error.message}
          <button onClick={retry}>Retry</button>
        </p>
      );
    case NetworkRequestType.failure:
      return <p style={{ textAlign: "center" }}>Success!</p>;
    default:
      return null;
  }
}

L’utilizzo di switch-case è più strettamente legato alla ramificazione di casi mutualmente esclusivi. If-else, complici anche le variazioni che sono state utilizzate storicamente, è uno strumento più versatile e, quindi, più ambiguo rispetto a questo caso d’uso specifico.

Come abbiamo detto, non possiamo usare switch-case con la classe NetworkRequest. Inoltre, abbiamo sempre bisogno di un caso di default, perché JavaScript non sa che abbiamo abbastanza confidenza per non averne bisogno. 

Il problema principale è che switch-case, in questo caso, implica una conoscenza della struttura interna della richiesta di rete di cui non vogliamo dare la responsabilità a chi utilizza il componente Query e neanche la nostra funzione hook useQuery, perché quel tipo di responsabilità è isolato all’interno di NetworkRequest. Come facciamo allora? Modifichiamo NetworkRequest!

Matching functions in React

In programmazione funzionale i protagonisti non sono gli oggetti, ma i verbi. Non ci importa cosa sono le cose, ma cosa sanno fare. Un’operazione molto comune in programmazione funzionale è quella del matching

L’operazione di matching è quella di prendere in considerazione tutti i casi possibili e definire una direzione diversa per ogni caso. If-else è un’operazione di matching con un valore booleano. Possiamo immaginare operazioni di matching per gruppi di valori, come stringhe vuote e non vuote, numeri negativi, nulli e positivi, array vuoti e non vuoti, valori nulli o non nulli, operazioni che hanno successo o fallimento.

Ecco un esempio di operazione di matching per la nostra classe NetworkRequest:

// File: src/hooks/NetworkRequest.js

export class NetworkRequest {
  // ...

  match({ whenIdle, whenLoading, whenSuccessful, whenFailure }) {
    if (this.isIdle()) {
      return whenIdle();
    } else if (this.isLoading()) {
      return whenLoading();
    } else if (this.isSuccessful()) {
      return whenSuccessful(this.#state.data);
    } else if (this.isFailure()) {
      return whenFailure(this.#state.error);
    } else {
      throw new Error(`Unknown state for NetworkRequest: ${this.#state}`);
    }
  }
}

Ed ecco come possiamo usarla:

// File: src/components/Query.tsx

import { useQuery } from "../hooks/useQuery";

export default function Query({ path, initialData }) {
  const [networkRequestState, retry] = useQuery({ path, initialData });

  return networkRequestState.match({
    whenIdle: () => null,
    whenLoading: () => <p style={{ textAlign: "center" }}>Loading...</p>,
    whenFailure: (error) => (
      <p style={{ textAlign: "center", color: "red" }}>
        Error! {error.message}
        <button onClick={retry}>Retry</button>
      </p>
    ),
    whenSuccessful: () => <p style={{ textAlign: "center" }}>Success!</p>,
  });
}

Possiamo notare che:

  • Il metodo match di NetworkRequest utilizza if-else al suo interno, ma sapendo con certezza che #state può essere un’istanza di quattro possibili classi e nient’altro se non quello, possiamo permetterci di lanciare un errore dentro else perché non succederà mai. NetworkRequest isola tutte le sue funzionalità e non può essere modificata senza una conoscenza approfondita del suo funzionamento e, in più, descrive tutti e soli gli stati di una richiesta di rete. Non cambierà funzionamento a meno che non cambi la tecnologia con cui funzionano le richieste di rete – che, per inciso, funzionano così dal 2000 – caso in cui andrebbe rivista probabilmente l’intera applicazione.
  • Il metodo match di NetworkRequest ci costringe a prendere sempre in considerazione tutti i casi disponibili. Questo è essenziale perché sappiamo che la richiesta di rete passa per almeno tre dei quattro casi ogni volta (“idle”, anche se, nel caso di query, per molto poco; “loading” sempre; almeno uno tra “success” o “failure”). 

IIFE/IIAE in React

L’ultima cosa che ci potrebbe servire per utilizzare la tecnica del rendering condizionale insieme alla programmazione dichiarativa, è un metodo per assegnare direttamente il risultato di un if-else o di uno switch-case a una variabile.

Immaginiamo un caso del genere:

export default function WeekDay({ weekDayNumber }) {
  switch (weekDayNumber) {
    case 0:
      return <p>Monday</p>;
    case 1:
      return <p>Tuesday</p>;
    case 2:
      return <p>Wednesday</p>;
    case 3:
      return <p>Thursday</p>;
    case 4:
      return <p>Friday</p>;
    default:
      return null;
  }
}

Riceviamo un numero da 0 a 4 e lo trasformiamo nel nome di un giorno della settimana. Facile! Ora, però, vorremmo isolare quell’elemento p uguale per tutti, e chiudere tutto dentro un div per poterlo selezionare in CSS. Quello che si fa di solito è dichiarare una variabile, popolarla tramite lo switch-case, per poi iniettarla in JSX, in questo modo:

export default function WeekDay({ weekDayNumber }) {
  let paragraph;

  switch (weekDayNumber) {
    case 0:
      paragraph = "Monday";
    case 1:
      paragraph = "Tuesday";
    case 2:
      paragraph = "Wednesday";
    case 3:
      paragraph = "Thursday";
    case 4:
      paragraph = "Friday";
    default:
      paragraph = null;
  }

  return (
    <div className="WeekDay">
      <p>{paragraph}</p>
    </div>
  );
}

La forma nell’esempio soprastante è perfettamente valida, ma non particolarmente fluida:

la variabile paragraph compare due volte più una per ogni possibile valore che la prop weekDayNumber può assumere. In più, essendo una variabile, chiunque potrebbe assegnarle un valore diverso in qualsiasi punto del codice, consapevolmente o per errore. Per non parlare del fatto che, trattandosi di linguaggio JavaScript, quel valore potrebbe essere qualsiasi cosa (un numero, null, un array con duemila elementi, un’emoticon, un pacchetto di caramelle..)

In questo esempio molto semplice la cosa sarebbe immediatamente evidente, ma immaginiamo un esempio più complesso, in cui ci sono dieci di questi casi.

Se saltasse fuori un bug per cui paragraph ha valore “July”, perché qualcuno l’ha scambiata per qualcos’altro (il nome paragraph è volutamente ambiguo), dovremmo andare a trovare tutti i punti in cui compare paragraph, seguire tutti i valori che le sono stati assegnati e capire dove, e soprattutto perché, ha ricevuto un valore sbagliato.

Visto che il nostro obiettivo è quello di metterci nelle condizioni di non poter sbagliare, la soluzione perfetta sarebbe quella che ci permette di:

  • Calcolare il valore di paragraph in un solo punto, così che non debba più essere modificata, rendendola una costante.
  • Effettuare il calcolo esattamente dove ci serve, così da non dover nemmeno dichiarare una costante da iniettare in JSX, per cui doverci inventare un nome, che potrebbe essere ambiguo.

In alcuni linguaggi di programmazione, è possibile assegnare il risultato di un blocco condizionale (if-else o switch-case). Questo, per esempio, è un esempio in Rust, in cui l’operazione di switch-case si chiama (guarda caso) match:

let week_day_string = match week_day_number {
    0 => "Monday",
    1 => "Tuesday",
    2 => "Wednesday",
    3 => "Thursday",
    4 => "Friday",
    _ => "",
};

In JavaScript, purtroppo, non ci è permesso scrivere una cosa del genere:

// Errore di sintassi!
const paragraph = switch (weekDayNumber) {
  case 0:
    paragraph = "Monday";
  case 1:
    paragraph = "Tuesday";
  case 2:
    paragraph = "Wednesday";
  case 3:
    paragraph = "Thursday";
  case 4:
    paragraph = "Friday";
  default:
    paragraph = null;
}

Di conseguenza, anche questo non ci è concesso:

export default function WeekDay({ weekDayNumber }) {
  // Errore di sintassi!
  return (
    <div className="WeekDay">
      {switch (weekDayNumber) {
        case 0:
          return "Monday";
        case 1:
          return "Tuesday";
        case 2:
          return "Wednesday";
        case 3:
          return "Thursday";
        case 4:
          return "Friday";
        default:
          return null;
      }}
    </div>
  );
}

In compenso, il linguaggio JavaScript è un linguaggio interpretato, il che ci permette di dichiarare funzioni “al volo” ed eseguirle istantaneamente in totale libertà. Queste funzioni si chiamano Immediately Invoked Function Expressions (espressioni funzionali invocate immediatamente), spesso abbreviate con l’acronimo IIFE, e funzionano così:

  1. Definiamo una funzione anonima, per esempio:
    function() {
      return "Hello World!";
    }
  2. La abbracciamo tra due parentesi tonde. Queste non hanno nessun effetto da un punto di vista sintattico (in JavaScript (((((2))))) === 2), ma la trasformano in un valore:
    (function () {
      return "Hello World!";
    });
  3. Ora che abbiamo un valore, la chiamiamo. Questo la eseguirà istantaneamente, trasformandola nel suo valore di ritorno:
    (function () {
      return "Hello World!";
    })();
  4. Quello che è scritto nell’esempio del punto 3. è equivalente alla stringa “Hello World!”, il che vuol dire che possiamo assegnarla a una costante:
    const helloWorld = (function () {
      return "Hello World!";
    })();

Il codice soprastante è equivalente a const helloWorld = “Hello World!”, ma ci dà l’opportunità di usare una funzione per restituire il valore, che vuol dire che possiamo inserire logiche più complesse, per esempio proprio un if-else o uno switch-case.

Possiamo usare una IIFE per rivedere il nostro componente di esempio:

export default function WeekDay({ weekDayNumber }) {
  return (
    <div className="WeekDay">
      <p>
        {(function () {
          switch (weekDayNumber) {
            case 0:
              return "Monday";
            case 1:
              return "Tuesday";
            case 2:
              return "Wednesday";
            case 3:
              return "Thursday";
            case 4:
              return "Friday";
            default:
              return null;
          }
        })()}
      </p>
    </div>
  );
}

Ed ecco che possiamo effettuare i nostri calcoli dove ci servono, senza dichiarare costanti e senza rischiare di sbagliare!

La stessa cosa può essere fatta con le arrow function, creando Immediately Invoked Arrow Expressions (IIAE), con lo stesso procedimento:

() => "Hello World!";.
(() => "Hello World!");.
(() => "Hello World!")();.
const helloWorld = (() => "Hello World!")();.
export default function WeekDay({ weekDayNumber }) {
  return (
    <div className="WeekDay">
      <p>
        {(() => {
          switch (weekDayNumber) {
            case 0:
              return "Monday";
            case 1:
              return "Tuesday";
            case 2:
              return "Wednesday";
            case 3:
              return "Thursday";
            case 4:
              return "Friday";
            default:
              return null;
          }
        })()}
      </p>
    </div>
  );
}

Il metodo delle IIFE o IIAE è un’ottima alternativa alle funzioni di matching, che fanno comunque lo stesso lavoro con gli stessi vantaggi:

function matchWeekDayNumber(
  weekDayNumber,
  { when0, when1, when2, when3, when4 }
) {
  switch (weekDayNumber) {
    case 0:
      return when0();
    case 1:
      return when1();
    case 2:
      return when2();
    case 3:
      return when3();
    case 4:
      return when4();
    default:
      throw new Error(`Invalid week day number: ${weekDayNumber}`);
  }
}

export default function WeekDay({ weekDayNumber }) {
  return (
    <div className="WeekDay">
      <p>
        {matchWeekDayNumber(weekDayNumber, {
          when0: () => "Monday",
          when1: () => "Tuesday",
          when2: () => "Wednesday",
          when3: () => "Thursday",
          when4: () => "Friday",
        })}
      </p>
    </div>
  );
}

Le funzioni di matching sono più utili in casi più complessi, quando le logiche interne di una parte di applicazione sono isolate e vengono riutilizzate, come nel caso della nostra classe NetworkRequest. Quando si tratta di casi una tantum o particolarmente semplici, o al contrario quando la logica per calcolare un valore non è un if-else o uno switch-case, ma qualcosa di più complesso (che non ha necessariamente a che fare con le macchine a stati finiti), le IIFE/IIAE sono la soluzione più indicata.

Operatori ternari in React

Se sei arrivato fin qui, probabilmente avrai pensato almeno una volta: “e gli operatori ternari?” Detto fatto! In questo paragrafo ci occuperemo proprio di quelli.

Gli operatori ternari ci permettono di esprimere su una sola riga o assegnare a una variabile il risultato di un if-else, per esempio:

export default function MyMessage({ messageString }) {
  return (
    <div className="MyMessage">
      {messageString === "" ? null : <p>{messageString}</p>}
    </div>
  );
}

Alcuni programmatori informatici non sono grandi fan degli operatori ternari, perché li reputano come “frasi che contengono troppo poca punteggiatura”. Finché si tratta di if-else semplici, non è troppo difficile leggerli, ma spesso si preferisce di gran lunga questa forma:

export default function MyMessage({ messageString }) {
  return (
    <div className="MyMessage">
      {(() => {
        if (messageString === "") {
          return null;
        } else {
          return <p>{messageString}</p>;
        }
      })()}
    </div>
  );
}

All’inizio, le righe che dicono {(() => { e })()} possono confondere, con tutte quelle parentesi di fila, ma ti assicuriamo che, dopo un po’, comincerai a escluderle in automatico quando leggerai il codice.

L’esempio soprastante è un caso limite, ma prendiamo un caso più complesso:

export function Age({ birthday }) {
  const now = new Date();
  const differenceYears = user.dateOfBirth.getFullYear() - now().getFullYear();
  const differenceMonths = user.dateOfBirth.getMonth() - now().getMonth();
  const differenceDays = user.dateOfBirth.getDate() - now().getDate();

  return (
    <div className="Age">
      <p>
        Your age is:{" "}
        <span>
          {differenceMonths > 0
            ? differenceYears
            : differenceMonths === 0
            ? differenceDays > 0
              ? differenceYears
              : differenceYears - 1
            : differenceYears - 1}
        </span>
      </p>
    </div>
  );
}

Anche con Prettier che ce la mette tutta per formattare il codice, questo esempio è quasi illeggibile. L’algoritmo che rappresenta non è dei più semplici di per sé e l’ammasso di ? e : peggiora la situazione. La forma IIAE è incredibilmente migliore:

export function Age({ birthday }) {
  const now = new Date();
  const differenceYears = user.dateOfBirth.getFullYear() - now().getFullYear();
  const differenceMonths = user.dateOfBirth.getMonth() - now().getMonth();
  const differenceDays = user.dateOfBirth.getDate() - now().getDate();

  return (
    <div className="Age">
      <p>
        Your age is:{" "}
        <span>
          {(() => {
            if (differenceMonths > 0) {
              return differenceYears;
            } else if (differenceMonths === 0) {
              if (differenceDays > 0) {
                return differenceYears;
              } else {
                return differenceYears - 1;
              }
            } else {
              return differenceYears - 1;
            }
          })()}
        </span>
      </p>
    </div>
  );
}

Dunque, gli operatori ternari pur essendo la scelta più semplice quando si tratta di if-else “in un colpo solo”, come abbiamo appena visto, non sono sempre la scelta migliore. Questa è una questione di preferenze, non una regola: sentiti libero di utilizzare la forma che più ti piace!

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