CONTATTACI

Guide per aspiranti programmatori

Programmatore che esamina react al microscopio
Lezione 36 / 41

Context in React

Per fare un esempio dell’utilizzo di Context, immaginiamo di voler implementare una funzione di login, per proteggere la nostra applicazione e anche per creare liste di cose da fare legate a utenti diversi.

Implementiamo prima di tutto il form che, grazie alle nostre funzioni hook useForm e useCommand, alla nostra macchina a stati finiti NetworkRequest e ai nostri componenti Form e TextInput, sarà un passeggiata:

 

// File: src/components/LoginForm.jsx

import "./LoginForm.css";
import { useCommand } from "../hooks/useCommand";
import { useForm } from "../hooks/useForm";
import { nonEmptyStringValidator } from "../validators";
import Form from "./Form";
import TextInput from "./TextInput";
import Panel from "./Panel";

export default function LoginForm({ onLogin }) {
  const [loginNetworkRequest, login] = useCommand({
    path: "/users/login",
    method: "POST",
  });

  const { inputProps, onSubmit } = useForm({
    fields: {
      email: {
        initialValue: "",
        validator: nonEmptyStringValidator("The email cannot be empty!"),
      },
      password: {
        initialValue: "",
        validator: nonEmptyStringValidator("The password cannot be empty!"),
      },
    },
    submit: (data) => {
      login(data).then((response) => {
        onLogin(response);
      });
    },
  });

  return (
    <div className="LoginForm">
      <Panel>
        <Form onSubmit={onSubmit} isLoading={loginNetworkRequest.isLoading()}>
          <TextInput
            {...inputProps("email")}
            placeholder="Email"
            type="email"
          />
          <TextInput
            {...inputProps("password")}
            placeholder="Password"
            type="password"
          />
        </Form>
      </Panel>
    </div>
  );
}

Non abbiamo creato un validatore isEmail, ma passando type=”email” a TextInput – che ci permette di inviare attributi direttamente all’elemento <input /> tramite props – sfruttiamo la validazione nativa del browser. Non è una soluzione particolarmente elegante, ma è utile ai fini del nostro esempio.

Il resto è un misto di useCommand e NetworkRequest, useForm e il componente Form. Scateniamo la potenza dei componenti riutilizzabili e delle funzioni hook per sviluppare un intero componente con validazione, invio e sincronizzazione dello stato della richiesta di rete con l’interfaccia. Ci abbiamo addirittura messo Panel, il nostro componente view per eccellenza, per prendere una scorciatoia sull’aspetto visivo.

Poche righe di linguaggio CSS et.. voilà:

 

/* File: src/components/LoginForm.css */

.LoginForm .TextInput + .TextInput,
.LoginForm button {
  margin-top: 0.5em;
}

La prop onLogin comunica al componente controller di LoginForm che il login è avvenuto con successo. Possiamo collegare questa prop al nostro componente App:

 

// File: src/components/App.jsx

// ...

import LoginForm from "./components/LoginForm";
import { useState } from "react";

// ...

export default function App() {
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  // ...

  if (isLoggedIn) {
    return {
      /* ... */
    };
  } else {
    const onLogin = () => {
      setIsLoggedIn(true);
    };

    return <LoginForm onLogin={onLogin} />;
  }
}

Aggiungiamo uno stato al nostro componente App, che traccia lo stato di autenticazione dell’utente. Con il rendering condizionale, mostriamo la lista di cose da fare all’utente autenticato e il form a quello anonimo.

Ora, immaginiamo di voler inserire un bottone “Logout” all’interno del componente TodoList. Dovremmo passare una funzione logout come prop a TodoList e chiamarla al click del bottone, all’interno del componente. In questo caso, questo passaggio non sarebbe un grosso problema ma, come dicevamo prima, alla lunga, con l’aggiunta di livelli di profondità e di funzionalità, la cosa potrebbe sfuggire di mano.

Proviamo. allora. a utilizzare Context.

 

Come funziona Context?

Context (che, letteralmente, vuol dire “contesto”) funziona a due livelli: il Provider (la cui traduzione è “fornitore”) e il/i Consumer (tradotto, appunto, “consumatore”).
Ogni Context ha un solo Provider, ma può avere più di un Consumer.

Il Provider di un Context contiene solitamente uno stato.
Tutti i Consumer accedono allo stesso stato.

Per il resto, Context funziona come il resto in React: lo stato viene passato ai Consumer e i cambiamenti vengono segnalati tramite eventi.

Cominciamo dal Provider.
Possiamo creare una nuova cartella contexts e inserire un nuovo file AuthContext.jsx al suo interno. L’estensione jsx serve perché il Provider esporta un componente React:

 

// File: src/contexts/AuthContext.jsx

import { createContext, useContext, useState } from "react";

const AuthContextStatus = {
  anonymous: "anonymous",
  loggedIn: "loggedIn",
};

const AuthContext = createContext({
  isLoggedIn: () => false,
  login: () => {},
  logout: () => {},
});

export function AuthContextProvider({ children }) {
  const [authStatus, setAuthStatus] = useState({
    status: AuthContextStatus.anonymous,
  });

  const login = () => {
    setAuthStatus({
      status: AuthContextStatus.loggedIn,
    });
  };

  const logout = () => {
    setAuthStatus({
      status: AuthContextStatus.anonymous,
    });
  };

  const isLoggedIn = () => {
    return authStatus.status === AuthContextStatus.loggedIn;
  };

  return (
    <AuthContext.Provider value={{ isLoggedIn, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useAuthContext() {
  return useContext(AuthContext);
}

Ci sono un bel po’ di cose nuove in questo file, vediamole per passi:

 

// File: src/contexts/AuthContext.jsx

// ...

const AuthContext = createContext({
  isLoggedIn: () => false,
  login: () => {},
  logout: () => {},
});

// ...

createContext è la funzione fornita da React che ci permette di creare il Context, lo “stato globale”. createContext accetta un solo argomento, che rappresenta il valore predefinito dello stato del Context di riferimento (nel nostro caso AuthContext). Possiamo associare l’argomento accettato da createContext a quello accettato da useState, un valore iniziale.

A differenza dell’argomento accettato da useState, però, il valore predefinito passato a createContext non verrà utilizzato, perché verrà sostituito dal valore che daremo alla prop value di AuthContext.Provider prima che il Context venga utilizzato per la prima volta.

Il motivo per cui createContext accetta un valore iniziale è legato a TypeScript, non siamo costretti a passare un valore iniziale. Farlo, però, ci aiuta a prendere nota di che forma vogliamo che abbia il nostro Context, e aiuta anche chi legge il nostro codice a capirlo senza dover cercare il punto in cui creiamo il Provider.  Se vuoi approfondire, ecco un’interessante discussione con l’intervento del team di sviluppo di React.

Dall’interno, AuthContext avrà uno status e due eventi, login e logout. Possiamo facilmente immaginare lo schema.

FSM – Autenticazione

status rappresenta lo stato (“anonimo” o “autenticato”), login e logout rappresentano gli eventi che scatenano le transizioni.

All’esterno, invece, non restituiamo lo status, che ha valori gestiti internamente, ma una funzione isLoggedIn che restituisce true se l’utente è autenticato, false se è anonimo.

Diamo al valore predefinito una forma simile a quella che avrà il valore effettivo di AuthContext. isLoggedIn restituisce false, per ricordarci che è una funzione che restituisce un valore booleano, e anche che lo stato iniziale della macchina a stati finiti sarà “anonimo”.

login e logout saranno due funzioni che non accetteranno nessun argomento e non restituiranno nessun valore, perché il loro compito sarà scatenare le transizioni della macchina a stati finiti e il risultato delle transizioni sarà un cambiamento di status e, di conseguenza, del valore di ritorno di isLoggedIn.

Infine, creiamo un oggetto AuthContextStatus per aiutarci a ricordare i valori che lo status può assumere.

Nota: in un’applicazione realistica, login accetterebbe come argomento le credenziali dell’utente, come il token di autenticazione (mai utilizzare la password!). status, nella sua forma “loggedIn“, avrebbe a sua volta le credenziali come proprietà.

 

// File: src/contexts/AuthContext.jsx

// ...

export function AuthContextProvider({ children }) {
  const [authStatus, setAuthStatus] = useState({
    status: AuthContextStatus.anonymous,
  });

  const login = () => {
    setAuthStatus({
      status: AuthContextStatus.loggedIn,
    });
  };

  const logout = () => {
    setAuthStatus({
      status: AuthContextStatus.anonymous,
    });
  };

  const isLoggedIn = () => {
    return authStatus.status === AuthContextStatus.loggedIn;
  };

  // ...
}

// ...

La prima parte di AuthContextProvider non è diversa da quella che potremmo avere con qualsiasi altro componente. authStatus è lo stato del componente. Il suo stato iniziale è “anonymous” perché, quando l’applicazione è visualizzata la prima volta, non sappiamo chi sia l’utente.

Nota: se volessimo salvare e leggere le credenziali dell’utente nell’archivio locale del browser, con useEffect, window.localStorage e gli esempi che abbiamo visto in precedenza, questo sarebbe il punto perfetto per farlo. Le credenziali rimarrebbero isolate all’interno di AuthContextProvider e di semplice accesso per qualsiasi componente attraverso l’uso di AuthContext, senza chiedersi da dove vengono.

login e logout sono due funzioni che gestiscono eventi, che corrispondono agli eventi che scatenano le transizioni della macchina a stati finiti. Le due funzioni chiamano la funzione setter setAuthStatus, causando un ciclo di rendering, gestendo il flusso di autenticazione in perfetto stile React.

 

// File: src/contexts/AuthContext.jsx

// ...

export function AuthContextProvider({ children }) {
  // ...

  return (
    <AuthContext.Provider value={{ isLoggedIn, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}
// ...

Questa è, forse, la parte più specifica rispetto al concetto di Context. Per permettere ai componenti di accedere a un Context, i componenti devono essere figli del componente Provider legato a quel Context. Tutte le istanze di Context, come il nostro AuthContext, sono degli oggetti con una proprietà Provider che è un componente. Provider accetta una prop – value – che contiene lo stato corrente del Context.

React tratta il nostro Provider come qualsiasi altro componente: quando la prop value viene aggiornata, tutti i figli del component Provider vengono chiamati al rendering.

Notiamo le doppie parentesi graffe in value={{ isLoggedIn, login, logout }}. Come abbiamo già visto in una delle prime sezioni, con l’attributo style, la coppia di parentesi graffe esterne serve per passare da JSX a JavaScript, mentre la coppia di parentesi graffe interne rappresenta il fatto che stiamo passando un oggetto come prop.

La chiamata risultante, per utilizzare l’oggetto che abbiamo passato alla prop value, è la seguente:

 

const { isLoggedIn, login, logout } = useContext(AuthContext);

Lo stesso oggetto che entra nella prop value esce dalla chiamata alla funzione hook useContext. Noi, però, aggiungeremo un ulteriore livello di astrazione.

 

Context e la separazione dei concetti

 

// File: src/contexts/AuthContext.jsx

// ...

export function useAuthContext() {
  return useContext(AuthContext);
}
useContext è la funzione hook che permette a qualsiasi componente figlio del Provider di accedere allo stato del Context. Riprendendo l’esempio iniziale:
<LanguageContext.Provider value={{ language, onLanguageChange }}>
  <App>
    <Router>
      <ProfilePage>
        <Settings>
          <Panel>
            <Select />
          </Panel>
        </Settings>
      </ProfilePage>
    </Router>
  </App>
</LanguageContext.Provider>

Grazie all’utilizzo di un ipotetico LanguageContext, il componente Select potrebbe utilizzare questa sintassi:

 

// ...

export default function Select(/* ... */) {
  // ...

  const { language, onLanguageChange } = useContext(LanguageContext);

  // ...
}

Contattaci senza impegno per informazioni sul corso

Pagamento rateale

Valore della rata: A PARTIRE DA 115 €/mese.

Esempio di finanziamento 

Importo finanziato: € 2440 in 24 rate da € 115 – TAN fisso 9,55% TAEG 12,57% – importo totale del credito € 2841.

Il costo totale del credito comprende: interessi calcolati al TAN indicato, oneri fiscali (imposta di bollo sul contratto 16,00 euro*) addebitati sulla prima rata, costo mensile di gestione pratica € 3,90, spesa di istruttoria € 0,00, spesa per invio rendicontazione periodica cartacea € 0,98 (o spesa per invio rendicontazione periodica cartacea € 0,00), imposta di bollo su rendicontazione periodica € 0,00. Modalità di rimborso obbligatoria: addebito diretto su c/c. La scadenza delle rate è determinata dal giorno della liquidazione del contratto; la data di scadenza delle rate è prevista il giorno 15 del mese. L’importo di ciascuna rata comprende una quota di capitale crescente e interessi decrescente secondo un piano di ammortamento “alla francese”. Offerta valida dal 01/01/2024 al 31/12/2024.

Messaggio pubblicitario con finalità promozionale. Per le informazioni precontrattuali richiedere sul punto vendita il documento “Informazioni europee di base sul credito ai consumatori” (SECCI) e copia del testo contrattuale. Salvo approvazione di Sella Personal Credit S.p.A. Aulab S.r.l. opera quale intermediario del credito NON in esclusiva.

*In fase di richiesta del finanziamento verrà proposta la facoltà di selezionare, in alternativa all’imposta di bollo sul contratto di 16,00 euro, l’imposta sostitutiva, pari allo 0,25% dell’importo finanziato.

Pagamento rateale

Valore della rata: A PARTIRE DA 210 €/mese.

Esempio di finanziamento  

Importo finanziato: € 4500 in 24 rate da € 210,03 – TAN fisso 9,68% TAEG 11,97% – importo totale del credito € 5146,55.

Il costo totale del credito comprende: interessi calcolati al TAN indicato, oneri fiscali (imposta di bollo sul contratto 16,00 euro*) addebitati sulla prima rata, costo mensile di gestione pratica € 3,90, spesa di istruttoria € 0,00, spesa per invio rendicontazione periodica cartacea € 0,98 (o spesa per invio rendicontazione periodica cartacea € 0,00), imposta di bollo su rendicontazione periodica € 0,00. Modalità di rimborso obbligatoria: addebito diretto su c/c. La scadenza delle rate è determinata dal giorno della liquidazione del contratto; la data di scadenza delle rate è prevista il giorno 15 del mese. L’importo di ciascuna rata comprende una quota di capitale crescente e interessi decrescente secondo un piano di ammortamento “alla francese”. Offerta valida dal 01/01/2024 al 31/12/2024.

Messaggio pubblicitario con finalità promozionale. Per le informazioni precontrattuali richiedere sul punto vendita il documento “Informazioni europee di base sul credito ai consumatori” (SECCI) e copia del testo contrattuale. Salvo approvazione di Sella Personal Credit S.p.A. Aulab S.r.l. opera quale intermediario del credito NON in esclusiva.

* In fase di richiesta del finanziamento verrà proposta la facoltà di selezionare, in alternativa all’imposta di bollo sul contratto di 16,00 euro, l’imposta sostitutiva, pari allo 0,25% dell’importo finanziato.

Contattaci senza impegno per informazioni sul corso

Scopriamo insieme se i nostri corsi fanno per te. Compila il form e aspetta la chiamata di uno dei nostri consulenti.