React: useActionState e useFormStatus

React 19 introduce diverse nuove funzionalità interessanti, tra cui l’hook useActionState. Questo hook sperimentale mira a semplificare la gestione dello stato e delle azioni nei componenti React, in particolare per i moduli. In questo articolo, esploreremo useActionState confrontandolo con l’hook ben noto useState, con alcuni esempi di codice per entrambi e discutendo i loro pro e contro.

Confronto tra useActionState e useState

Consideriamo un semplice modulo di login con campi per nome utente e password.

Esempio con useState:

import React, { useState } from 'react';

function App() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const [response, setResponse] = useState(null);
  const [isPending, setIsPending] = useState(false);
  const [errors, setErrors] = useState(null);

  const handleSubmit = async (event) => {
    event.preventDefault();

    // Imposto gli stati prima di inviare la richiesta
    setResponse(null);
    setIsPending(true);
    setErrors(null);

    // Invio i dati del modulo al server, simulo un ritardo di risposta 
    await new Promise((resolve) => setTimeout(resolve, 2000));

    // Imposto gli stati dopo la risposta in base a cosa ricevo
    // ovviamente in caso di risposte vere sarebbe da controllare eventuali errori
    setResponse('done!');
    setIsPending(false);
    setErrors(null); 
  };

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <label>Nome utente:</label>
        <input
          type="text"
          value={username}
          onChange={(e) => setUsername(e.target.value)}
        />
        <label>Password:</label>
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
        />
        <button type="submit">Accedi</button>
      </form>

      
      <div>Risposta: {response}</div>
      <div>Stato: {isPending ? 'caricamento...' : 'pronto'}</div>
    </div>
  );
}

export default App

Esempio con useActionState:

import React, { useActionState } from 'react';

function App() {
  const sendRequest = async (prevState, formData) => {
    const username = formData.get("username");
    const password = formData.get("password");

    // simulo l'invio di dati al server con un ritardo
    // qua potrei usare una fetch request che invia username e password
    await new Promise((resolve) => setTimeout(resolve, 2000));

    return {data: 'done!', error: null}
    // in caso di errore invece avrei avuto qualcosa di simile
    // return {data: prevState, error: 'Errore!'}
  }

  const [userData, submitAction, isPending] = useActionState(sendRequest, {
    data: null, 
    error: null,
  });

  return (
    <div>
      <form action={submitAction}>
        <label>Nome utente:</label>
        <input type="text" name="username" />

        <label>Password:</label>
        <input type="password" name="password" />

        <button type="submit">Accedi</button>
      </form>

      
      <div>Risposta: {userData.data}</div>
      <div>Stato: {isPending ? 'caricamento...' : 'pronto'}</div>
    </div>
  );
}

export default App

Cosa cambia tra la versione con useState e quella con useActionState

Vediamo come cambia il nostro codice così da comprendere poi le migliorie.

  • Non abbiamo più la necessità di gestire gli stati dei vari campi di input, l’importante è dare a tutti il corretto name. Via quindi value e onChange => form più pulito;
  • Non dobbiamo più gestire gli stati del form, questi sono già gestiti dal nuovo hook e vedremo che abbiamo a disposizione anche ulteriori vantaggi con l’uso di useFormStatus;
  • Allo stesso modo vengono esposti gli errori che l’invio dei dati eventualmente genera.

Il nuovo hook useFormStatus

Abbiamo anche un ulteriore vantaggio. Possiamo usare l’hook useFormStatus per intercettare lo stato del form e ad esempio modificare il comportamento di bottoni o altri elementi del nostro form.

import { useFormStatus } from 'react-dom'; // attenzione, da `react-dom` non da `react`

const SubmitButton = () => {
  const { pending } = useFormStatus();

  return <button type="submit">{pending ? 'Invio in corso...' : 'Invia'}</button>
}

Questo hook lo possiamo utilizzare in qualsiasi elemento figlio del form, a qualsiasi profondità!
Modifichiamo il codice per includere anche questo componente.

import React, { useActionState } from 'react';
import { useFormStatus } from 'react-dom';

const SubmitButton = () => {
  const { pending } = useFormStatus();

  return <button type="submit">{pending ? 'Invio in corso...' : 'Invia'}</button>
}

function App() {
  const sendRequest = async (prevState, formData) => {
    const username = formData.get("username");
    const password = formData.get("password");

    // simulo l'invio di dati al server con un ritardo
    // qua potrei usare una fetch request che invia username e password
    await new Promise((resolve) => setTimeout(resolve, 2000));

    return {data: 'done!', error: null}
    // in caso di errore invece avrei avuto qualcosa di simile
    // return {data: prevState, error: 'Errore!'}
  }

  const [userData, submitAction, isPending] = useActionState(sendRequest, {
    data: null, 
    error: null,
  });

  return (
    <div>
      <form action={submitAction}>
        <label>Nome utente:</label>
        <input
          type="text"
          name="username"
        />
        <label>Password:</label>
        <input
          type="password"
          name="password"
        />
        <SubmitButton />
      </form>

      
      <div>Risposta: {userData.data}</div>
      <div>Stato: {isPending ? 'caricamento...' : 'pronto'}</div>
    </div>
  );
}

export default App

Ora quando clicchiamo sul bottone “Invia”, mentre il form sta caricando i dati, avremo la scritta “Invio in corso…“. Terminato il caricamento il bottone tornerà a mostrare “Invia”. Carino vero? Questo hook inoltre espone anche altri dati che possiamo utilizzare, sempre a qualsiasi livello!

const { pending, data, method, action } = useFormStatus();

Come vedete abbiamo a disposizione tutte le informazioni che possono esserci utili per gestire al meglio il nostro form. E ci permette di comporre i nostri componenti in modo più pulito e indipendente, senza troppe props da propagare.

Conclusione

In entrambi gli esempi, lo stato del modulo viene gestito. Tuttavia, useActionState offre diversi vantaggi:

  • Integrazione di stato e azioni: useActionState combina lo stato del modulo e le azioni correlate in un’unica struttura, semplificando la gestione dello stato e riducendo il codice boilerplate;
  • Gestione automatica dell’invio del modulo: useActionState fornisce un’azione submit integrata che gestisce automaticamente l’invio del modulo e l’aggiornamento dello stato in base alla risposta del server;
  • Riduzione del codice: L’esempio con useActionState è più conciso e richiede meno codice per la gestione dello stato e delle azioni del modulo;

L’hook useActionState in React 19 rappresenta un passo avanti significativo nella gestione dei moduli, offrendo un’interfaccia più semplice e coesa per lo stato e le azioni. Sebbene sia ancora una funzionalità sperimentale, il suo potenziale per semplificare lo sviluppo di moduli React è evidente.

Siamo ancora in fase beta, ma se volete testarlo basta installare le versioni beta di react e di react-dom come abbiamo visto nell’introduzione.

Buon coding 😉

Riferimenti