React 18 : rendu concurrent

🏷️ Front-end 📅 08/04/2026 09:00:00 👤 Mezgani said
React Concurrent Rendering Usetransition
React 18 : rendu concurrent

Comprendre les transitions et le rendu concurrent pour garder une UI fluide même avec des composants lourds.

Objectif de l'article

Comprendre les transitions et le rendu concurrent pour garder une UI fluide même avec des composants lourds.

A retenir: React 18 rend l'UI fluide même avec des composants lourds en séparant les mises à jour urgentes (input, clic) des mises à jour non-urgentes (filtres, tri). Utilisez useTransition et useDeferredValue pour garder le contrôle sur les priorités.

Concepts clés

React 18 introduit le rendu concurrent pour améliorer la réactivité de l'UI. Voici les concepts essentiels :

  • Concurrent Rendering : Capacité de React à interrompre le rendu pour traiter les événements utilisateur de haute priorité
  • useTransition : Hook pour marquer une mise à jour comme non-urgente (background updates)
  • useDeferredValue : Hook pour créer une valeur différée d'une dépendance
  • Automatic Batching : React groupe les mises à jour pour réduire les rendus
  • Suspense : Composant pour gérer l'attente des ressources asynchrones
  • Priorités de mise à jour : Urgent (événements utilisateur) vs Non-urgent (données en arrière-plan)
  • Priorité composant : Isoler les composants lourds pour ne pas bloquer l'UI

Implémentation

1. Utiliser useTransition pour les mises à jour en arrière-plan

useTransition permet de marquer une mise à jour comme non-urgente. L'UI reste réactive pendant que React processe la mise à jour en arrière-plan :

import { useTransition, useState } from 'react';

export function SearchUsers() {
  const [searchTerm, setSearchTerm] = useState('');
  const [isPending, startTransition] = useTransition();
  const [results, setResults] = useState([]);

  const handleSearch = (e) => {
    const value = e.target.value;
    setSearchTerm(value); // Mise à jour urgente (input)

    // Marquer la recherche comme non-urgente
    startTransition(() => {
      const filtered = expensiveSearch(value);
      setResults(filtered);
    });
  };

  return (
    <div>
      <input
        value={searchTerm}
        onChange={handleSearch}
        placeholder="Chercher..."
      />
      {isPending && <span>Recherche en cours...</span>}
      <ul>
        {results.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

2. Utiliser useDeferredValue pour les valeurs différées

useDeferredValue crée une version différée d'une valeur, utile pour les listes longues :

import { useDeferredValue, useState } from 'react';

export function ProductList() {
  const [searchText, setSearchText] = useState('');
  const deferredSearchText = useDeferredValue(searchText);
  const [products] = useState(generateProducts());

  const filteredProducts = products.filter(p =>
    p.name.toLowerCase().includes(deferredSearchText.toLowerCase())
  );

  return (
    <div>
      <input
        value={searchText}
        onChange={(e) => setSearchText(e.target.value)}
        placeholder="Filtrer produits..."
      />
      <SlowList items={filteredProducts} />
    </div>
  );
}

function SlowList({ items }) {
  const startTime = performance.now();
  while (performance.now() - startTime < 1) {
    // Simule un composant lent
  }

  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.name}</li>
      ))}
    </ul>
  );
}

3. Isoler les composants lourds

Séparez les composants lourds dans leurs propres modules pour éviter que tout le rendu soit ralenti :

import { Suspense } from 'react';

function App() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<div>Chargement des données...</div>}>
        <HeavyDataComponent />
      </Suspense>
      {/* Les autres éléments restent réactifs */}
      <input placeholder="Je reste fluide" />
    </div>
  );
}

// Composant asynchrone avec Suspense
async function HeavyDataComponent() {
  const data = await fetchHeavyData();
  return <DataDisplay data={data} />;
}

4. Combiner useTransition et useDeferredValue

Approche complète pour une UI ultra-réactive avec filtre et liste :

import { useTransition, useDeferredValue, useState } from 'react';

export function AdvancedSearch() {
  const [input, setInput] = useState('');
  const [isPending, startTransition] = useTransition();
  const deferredInput = useDeferredValue(input);
  const [items] = useState(generateItems(1000));

  const filtered = items.filter(item =>
    item.title.toLowerCase().includes(deferredInput.toLowerCase())
  );

  const handleChange = (e) => {
    const value = e.target.value;
    setInput(value); // Urgent: mise à jour de l'input

    startTransition(() => {
      // Non-urgent: recalcul de la liste
      // (déjà fait via useDeferredValue, mais peut inclure une logique supplémentaire)
    });
  };

  return (
    <div>
      <input
        value={input}
        onChange={handleChange}
        placeholder="Chercher dans 1000 éléments..."
      />
      {isPending && <div className="loading">Traitement...</div>}
      <ItemList items={filtered} />
    </div>
  );
}

5. Suspense pour le chargement des données

Utilisez Suspense pour gérer l'attente avec une UI fluide :

import { Suspense } from 'react';

function UserProfile() {
  return (
    <div>
      <h1>Profil utilisateur</h1>
      <Suspense fallback={<div>Chargement du profil...</div>}>
        <UserData userId={123} />
      </Suspense>
      <Suspense fallback={<div>Chargement des articles...</div>}>
        <UserArticles userId={123} />
      </Suspense>
    </div>
  );
}

async function UserData({ userId }) {
  const user = await fetchUser(userId);
  return <div>Nom: {user.name}</div>;
}

async function UserArticles({ userId }) {
  const articles = await fetchArticles(userId);
  return (
    <ul>
      {articles.map(article => (
        <li key={article.id}>{article.title}</li>
      ))}
    </ul>
  );
}

6. Exemple complet : Liste avec pagination et filtres

import { useTransition, useDeferredValue, useState } from 'react';

export function DataTable({ data }) {
  const [filter, setFilter] = useState('');
  const [sortBy, setSortBy] = useState('name');
  const [isPending, startTransition] = useTransition();
  const deferredFilter = useDeferredValue(filter);
  const deferredSortBy = useDeferredValue(sortBy);

  const processedData = data
    .filter(item => item.name.includes(deferredFilter))
    .sort((a, b) => {
      const aVal = a[deferredSortBy];
      const bVal = b[deferredSortBy];
      return String(aVal).localeCompare(String(bVal));
    });

  return (
    <div>
      <input
        value={filter}
        onChange={(e) => setFilter(e.target.value)}
        placeholder="Filtrer..."
      />
      <select
        value={sortBy}
        onChange={(e) => startTransition(() => setSortBy(e.target.value))}
      >
        <option value="name">Nom</option>
        <option value="date">Date</option>
        <option value="status">Statut</option>
      </select>
      {isPending && <small>Mise à jour...</small>}
      <table>
        <tbody>
          {processedData.map(item => (
            <tr key={item.id}>
              <td>{item.name}</td>
              <td>{item.date}</td>
              <td>{item.status}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

Intégration en production

Bonnes pratiques pour React 18 Concurrent

  • Utiliser useTransition pour les mises à jour non-urgentes (filtres, tri, recherche)
  • useDeferredValue pour les valeurs qui alimentent des composants lents
  • Suspense boundaries : bien granulariser pour l'UX
  • Ne pas bloquer : éviter les calculs lourds synchrones dans les handlers
  • Profiler les performances : utiliser React DevTools Profiler
  • Code splitting : combiner avec Suspense pour le lazy loading
  • Mesurer Core Web Vitals : INP (Interaction to Next Paint)

Profiler avec React DevTools

Utilisez l'onglet Profiler pour analyser les performances :

// Dans React DevTools:
// 1. Ouvrir l'onglet "Profiler"
// 2. Enregistrer une interaction (clic, saisie)
// 3. Regarder les rendus : les transitions non-urgentes
//    doivent être marquées comme "Transition"
// 4. Identifier les composants trop lents à rendre

Pattern anti: Blocage du rendu

❌ À éviter : opération lourde dans un event handler :

// Mauvais: bloque l'UI
function BadSearch() {
  const [results, setResults] = useState([]);

  const handleSearch = (e) => {
    const term = e.target.value;
    const filtered = veryExpensiveSearch(term); // Bloque!
    setResults(filtered);
  };

  return <input onChange={handleSearch} />;
}

Pattern recommandé: Utiliser les transitions

✅ À faire : marquer comme transition :

// Bon: l'UI reste réactive
function GoodSearch() {
  const [searchTerm, setSearchTerm] = useState('');
  const [isPending, startTransition] = useTransition();
  const [results, setResults] = useState([]);

  const handleSearch = (e) => {
    const term = e.target.value;
    setSearchTerm(term); // Urgent

    startTransition(() => {
      const filtered = veryExpensiveSearch(term); // Non-blocking
      setResults(filtered);
    });
  };

  return (
    <div>
      <input value={searchTerm} onChange={handleSearch} />
      {isPending && <Spinner />}
    </div>
  );
}

Checkliste de migration vers Concurrent Features

  • ✓ Mettre à jour vers React 18+ et ReactDOM 18+
  • ✓ Identifier les opérations longues (> 50ms)
  • ✓ Envelopper dans startTransition ou useDeferredValue
  • ✓ Ajouter des indicateurs de chargement (isPending)
  • ✓ Tester sur de vrais appareils (mobiles lents)
  • ✓ Profiler avec DevTools avant/après
  • ✓ Mesurer INP en production (Web Vitals)
  • ✓ Documenter les transitions pour l'équipe

Mesurer les Core Web Vitals

import { getCLS, getFID, getFCP, getLCP, getTTFB, getINP } from 'web-vitals';

// INP est la métrique clé pour la réactivité
getINP(metric => {
  console.info(`INP: ${metric.value}ms`);
  // < 200ms = Good
  // 200-500ms = Needs Improvement
  // > 500ms = Poor
});

// Envoyer à votre service d'analytics
getLCP(metric => {
  sendToAnalytics({
    name: metric.name,
    value: metric.value,
    id: metric.id
  });
});

Erreurs courantes à éviter

  • ❌ Oublier d'appeler startTransition pour les mises à jour non-urgentes
  • ❌ Utiliser useTransition pour les mises à jour urgentes (input events)
  • ❌ Avoir trop de Suspense boundaries imbriquées
  • ❌ Ne pas tester sur des connexions lentes (3G)
  • ❌ Ignorer les avertissements de React Strict Mode
  • ❌ Oublier les fallbacks Suspense

Prochaines étapes

  • Apprendre React Server Components (RSC) pour le chargement de données
  • Utiliser Next.js avec App Router pour Suspense au niveau app
  • Implémenter le code splitting avec React.lazy et Suspense
  • Profiler et optimiser les composants coûteux
  • Monitorer les métriques Web Vitals en production