Gérer l’état

Intermédiaire

Au fur et à mesure que votre application se développe, il est utile de réfléchir à la façon dont vous organisez votre état et à la façon dont les données circulent entre vos composants. Les états redondants ou dupliqués sont une source fréquente de bugs. Dans ce chapitre, vous apprendrez à bien structurer votre état, à garder une logique de mise à jour de l’état maintenable, et à partager l’état entre des composants distants.

Réagir à la saisie avec un état

Avec React, vous ne modifierez pas l’interface utilisateur directement à partir du code. Par exemple, vous n’écrirez pas de commandes telles que « désactive le bouton », « active le bouton », « affiche le message de réussite », etc. Au lieu de ça, vous décrirez l’interface utilisateur que vous souhaitez voir apparaître pour les différents états visuels de votre composant (« état initial », « état de saisie », « état de réussite »), puis vous déclencherez les changements d’état en réponse aux interactions de l’utilisateur. Ça ressemble à la façon dont les designers réfléchissent à l’interface utilisateur.

Voici un formulaire de quiz construit avec React. Voyez comment il utilise la variable d’état status pour déterminer s’il faut activer ou désactiver le bouton d’envoi, et s’il faut plutôt afficher le message de réussite.

import { useState } from 'react';

export default function Form() {
  const [answer, setAnswer] = useState('');
  const [error, setError] = useState(null);
  const [status, setStatus] = useState('typing');

  if (status === 'success') {
    return <h1>C’est exact !</h1>
  }

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('submitting');
    try {
      await submitForm(answer);
      setStatus('success');
    } catch (err) {
      setStatus('typing');
      setError(err);
    }
  }

  function handleTextareaChange(e) {
    setAnswer(e.target.value);
  }

  return (
    <>
      <h2>Quiz sur les villes</h2>
      <p>
        Dans quelle ville trouve-t-on un panneau d’affichage qui transforme l’air en eau potable ?
      </p>
      <form onSubmit={handleSubmit}>
        <textarea
          value={answer}
          onChange={handleTextareaChange}
          disabled={status === 'submitting'}
        />
        <br />
        <button disabled={
          answer.length === 0 ||
          status === 'submitting'
        }>
          Submit
        </button>
        {error !== null &&
          <p className="Error">
            {error.message}
          </p>
        }
      </form>
    </>
  );
}

function submitForm(answer) {
  // Imaginez que ça fait une requête réseau
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      let shouldError = answer.toLowerCase() !== 'lima'
      if (shouldError) {
        reject(new Error('Bonne idée, mais mauvaise réponse. Réessayez !'));
      } else {
        resolve();
      }
    }, 1500);
  });
}

Prêt·e à en apprendre davantage ?

Lisez Réagir à la saisie avec un état pour apprendre à aborder les interactions dans une optique d’état.

En savoir plus

Choisir la structure de l’état

Une bonne structuration de l’état peut faire la différence entre un composant agréable à modifier et à déboguer, et un composant qui est une source constante de bugs. Le principe le plus important est que l’état ne doit pas contenir d’informations redondantes ou dupliquées. S’il y a des éléments d’état inutiles, il est facile d’oublier de les mettre à jour et d’introduire des bugs !

Par exemple, ce formulaire a une variable d’état fullName redondante :

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [fullName, setFullName] = useState('');

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
    setFullName(e.target.value + ' ' + lastName);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
    setFullName(firstName + ' ' + e.target.value);
  }

  return (
    <>
      <h2>Enregistrez-vous :</h2>
      <label>
        Prénom :{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Nom :{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        Votre billet sera au nom de : <b>{fullName}</b>
      </p>
    </>
  );
}

Vous pouvez la retirer et simplifier le code en calculant fullName à l’affichage du composant :

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  const fullName = firstName + ' ' + lastName;

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
  }

  return (
    <>
      <h2>Enregistrez-vous :</h2>
      <label>
        Prénom :{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Nom :{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        Votre billet sera au nom de : <b>{fullName}</b>
      </p>
    </>
  );
}

Ce changement peut sembler mineur, mais de nombreux bugs dans les applis React se corrigent ainsi.

Prêt·e à en apprendre davantage ?

Lisez Choisir la structure de l’état pour apprendre à architecturer votre état afin d’éviter les bugs.

En savoir plus

Partager l’état entre des composants

Parfois, vous souhaitez que l’état de deux composants change toujours en même temps. Pour cela, retirez l’état des deux composants, déplacez-le vers leur ancêtre commun le plus proche, puis transmettez-leur par l’intermédiaire des props. C’est ce qu’on appelle « faire remonter l’état », et c’est l’une des choses que vous ferez le plus souvent en écrivant du code React.

Dans l’exemple qui suit, seul un panneau devrait être actif à tout moment. Pour ce faire, au lieu de conserver l’état actif au sein de chaque panneau, le composant parent conserve l’état et spécifie les props de ses enfants.

import { useState } from 'react';

export default function Accordion() {
  const [activeIndex, setActiveIndex] = useState(0);
  return (
    <>
      <h2>Almaty, Kazakhstan</h2>
      <Panel
        title="À propos"
        isActive={activeIndex === 0}
        onShow={() => setActiveIndex(0)}
      >
        Avec une population d’environ 2 millions d’habitants, Almaty est la plus grande ville du Kazakhstan. De 1929 à 1997, elle en était la capitale.
      </Panel>
      <Panel
        title="Étymologie"
        isActive={activeIndex === 1}
        onShow={() => setActiveIndex(1)}
      >
        Le nom vient de <span lang="kk-KZ">алма</span>, le mot kazakh pour « pomme » et est souvent traduit comme « plein de pommes ». En fait, la région d’Almaty est considérée comme le berceau ancestral de la pomme, et la <i lang="la">Malus sieversii</i> sauvage est considérée comme l’ancêtre probable de la pomme domestique moderne.
      </Panel>
    </>
  );
}

function Panel({
  title,
  children,
  isActive,
  onShow
}) {
  return (
    <section className="panel">
      <h3>{title}</h3>
      {isActive ? (
        <p>{children}</p>
      ) : (
        <button onClick={onShow}>
          Afficher
        </button>
      )}
    </section>
  );
}

Prêt·e à en apprendre davantage ?

Lisez Partager l’état entre des composants pour apprendre comment faire remonter l’état et garder des composants synchronisés.

En savoir plus

Préserver et réinitialiser l’état

Lorsque vous rafraîchissez un composant, React doit décider quelles parties de l’arbre doivent être conservées (et mises à jour), et quelles parties doivent être supprimées ou recréées à partir de zéro. Dans la plupart des cas, le comportement automatique de React fonctionne assez bien. Par défaut, React préserve les parties de l’arbre qui « correspondent » avec l’arbre de composants du rendu précédent.

Cependant, il arrive que ce ne soit pas ce que vous souhaitez. Dans cette appli de discussion, le fait de taper un message puis de changer de destinataire ne réinitialise pas la saisie. L’utilisateur risque ainsi d’envoyer accidentellement un message à la mauvaise personne :

import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';

export default function Messenger() {
  const [to, setTo] = useState(contacts[0]);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedContact={to}
        onSelect={contact => setTo(contact)}
      />
      <Chat contact={to} />
    </div>
  )
}

const contacts = [
  { name: 'Thierry', email: 'thierry@mail.com' },
  { name: 'Alice', email: 'alice@mail.com' },
  { name: 'Bob', email: 'bob@mail.com' }
];

React vous permet d’outrepasser le comportement par défaut, et de forcer un composant à réinitialiser son état en lui passant une key différente, comme <Chat key={email} />. Ça dit à React que si le destinataire est différent, alors le composant Chat est considéré comme différent et doit être recréé à partir de zéro avec les nouvelles données (et l’interface utilisateur, par exemple les champs de saisie). À présent, passer d’un destinataire à l’autre réinitialise le champ de saisie, même si vous affichez le même composant.

import { useState } from 'react';
import Chat from './Chat.js';
import ContactList from './ContactList.js';

export default function Messenger() {
  const [to, setTo] = useState(contacts[0]);
  return (
    <div>
      <ContactList
        contacts={contacts}
        selectedContact={to}
        onSelect={contact => setTo(contact)}
      />
      <Chat key={to.email} contact={to} />
    </div>
  )
}

const contacts = [
  { name: 'Thierry', email: 'thierry@mail.com' },
  { name: 'Alice', email: 'alice@mail.com' },
  { name: 'Bob', email: 'bob@mail.com' }
];

Prêt·e à en apprendre davantage ?

Lisez Préserver et réinitialiser l’état pour apprendre le cycle de vie d’un état et comment le contrôler.

En savoir plus

Extraire la logique d’état dans un réducteur

Les composants comportant de nombreuses mises à jour d’état réparties entre de nombreux gestionnaires d’événements peuvent devenir intimidants. Dans un tel cas, vous pouvez consolider toute la logique de mise à jour de l’état en-dehors de votre composant dans une seule fonction, appelée « réducteur » (reducer, NdT). Vos gestionnaires d’événements deviennent concis car ils ne spécifient que les « actions » de l’utilisateur. Au bas du fichier, la fonction du réducteur spécifie comment l’état doit être mis à jour en réponse à chaque action !

import { useReducer } from 'react';
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(
    tasksReducer,
    initialTasks
  );

  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }

  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task
    });
  }

  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId
    });
  }

  return (
    <>
      <h1>Itinéraire à Prague</h1>
      <AddTask
        onAddTask={handleAddTask}
      />
      <TaskList
        tasks={tasks}
        onChangeTask={handleChangeTask}
        onDeleteTask={handleDeleteTask}
      />
    </>
  );
}

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [...tasks, {
        id: action.id,
        text: action.text,
        done: false
      }];
    }
    case 'changed': {
      return tasks.map(t => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter(t => t.id !== action.id);
    }
    default: {
      throw Error('Action inconnue : ' + action.type);
    }
  }
}

let nextId = 3;
const initialTasks = [
  { id: 0, text: 'Visiter le musée Kafka', done: true },
  { id: 1, text: 'Spectacle de marionnettes', done: false },
  { id: 2, text: 'Photo du mur Lennon', done: false }
];

Prêt·e à en apprendre davantage ?

Lisez Extraire la logique d’état dans un réducteur pour apprendre à consolider la logique au sein d’une fonction réducteur.

En savoir plus

Transmettre des données en profondeur avec le contexte

Habituellement, vous transmettez des informations d’un composant parent à un composant enfant par l’intermédiaire des props. Mais le passage des props peut s’avérer gênant si vous devez faire passer une information à travers plusieurs composants, ou si plusieurs composants ont besoin de la même information. Le contexte permet au composant parent de rendre certaines informations disponibles à n’importe quel composant de l’arbre situé en-dessous de lui—quelle que soit sa profondeur—sans avoir à les transmettre explicitement par le biais de props.

Ici, le composant Heading détermine son niveau d’en-tête en « demandant » son niveau à la Section la plus proche. Chaque Section détermine son propre niveau en demandant à la Section parente le sien, et en lui ajoutant un. Chaque Section fournit des informations à tous les composants situés en-dessous d’elle sans passer de props—elle le fait par le biais du contexte.

import Heading from './Heading.js';
import Section from './Section.js';

export default function Page() {
  return (
    <Section>
      <Heading>Titre</Heading>
      <Section>
        <Heading>En-tête</Heading>
        <Heading>En-tête</Heading>
        <Heading>En-tête</Heading>
        <Section>
          <Heading>Sous-en-tête</Heading>
          <Heading>Sous-en-tête</Heading>
          <Heading>Sous-en-tête</Heading>
          <Section>
            <Heading>Sous-sous-en-tête</Heading>
            <Heading>Sous-sous-en-tête</Heading>
            <Heading>Sous-sous-en-tête</Heading>
          </Section>
        </Section>
      </Section>
    </Section>
  );
}

Prêt·e à en apprendre davantage ?

Lisez Transmettre des données en profondeur avec le contexte pour apprendre à utiliser le contexte comme une alternative aux props.

En savoir plus

Mise à l’échelle en combinant réducteur et contexte

Les réducteurs vous permettent de consolider la logique de mise à jour de l’état d’un composant. Le contexte vous permet de transmettre des informations en profondeur à d’autres composants. Vous pouvez combiner les réducteurs et le contexte pour gérer l’état d’un écran complexe.

Avec cette approche, un composant parent ayant un état complexe le gère à l’aide d’un réducteur. D’autres composants situés à n’importe quel endroit de l’arbre peuvent lire son état via le contexte. Ils peuvent également envoyer des actions pour mettre à jour cet état.

import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import { TasksProvider } from './TasksContext.js';

export default function TaskApp() {
  return (
    <TasksProvider>
      <h1>Journée de repos à Kyoto</h1>
      <AddTask />
      <TaskList />
    </TasksProvider>
  );
}

Prêt·e à en apprendre davantage ?

Lisez Mise à l’échelle en combinant réducteur et contexte pour apprendre comment adapter la gestion d’état à une application en pleine croissance.

En savoir plus

Et maintenant ?

Allez sur Réagir à la saisie avec un état pour commencer à lire ce chapitre page par page !

Ou alors, si vous êtes déjà à l’aise avec ces sujets, pourquoi ne pas explorer les échappatoires?