Context in React | Aulab

GUIDE PER ASPIRANTI PROGRAMMATORI

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…

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

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);

  // ...
}

Hai bisogno di informazioni? 🙋🏻‍♂️

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