Décomposer les composants React de grande taille sans provoquer de régressions en production
Une approche étape par étape pour décomposer en toute sécurité de gros composants React : définition des responsabilités, extraction de la logique pure, gestion de l’état, et déploiement par petites étapes.
Sources: React Components Are Getting Too Big: How to Refactor Without Breaking Production
Toute base de code React mature possède un tel composant : un composant qui fonctionne, génère des revenus et fait peur à tous ceux qui doivent s’en occuper. Il a commencé comme une simple page, puis a intégré du filtrage, de la pagination, des appels API, des permissions, une validation, des dialogues, des exportations, des outils d’analyse ainsi qu’une liste croissante d’effets. Ce guide montre comment réduire la complexité de ce type de composant, plutôt que simplement sa taille de fichier, et comment le faire étape par étape sans jamais mettre en péril son fonctionnement en production.
Un exemple typique a progressivement intégré toutes les fonctionnalités associées à cette caractéristique :
function ProductManagement() {
// API calls
// Redux selectors
// Local state
// Filters
// Pagination
// Sorting
// Selection
// Permissions
// Validation
// Business rules
// Export
// Modal handling
// Analytics
// Hundreds of lines of JSX
}
Lorsque le nombre de lignes dépasse mille, on pense immédiatement que le problème réside dans la longueur. Ce n’est pas le cas. Un composant long n’est pas automatiquement un problème ; c’est plutôt un composant présentant de nombreuses raisons indépendantes de nécessiter des modifications qui en devient un. Le travail décrit ci-dessous ne consiste pas à diviser un fichier en dix, mais à réduire le coût et les risques des prochaines modifications.
Le comptage des lignes est la mauvaise métrique
Des règles telles que « refactoriser tout ce qui dépasse 300 lignes » sont faciles à appliquer mais peu efficaces pour identifier les vrais risques. Imaginez 500 lignes principalement composées de balises déclaratives avec peu de logique. Imaginez maintenant 200 lignes qui combinent des appels API, des transitions d’état, des vérifications de permissions, des validations, des effets, des règles métier et un rendu conditionnel imbriqué. Le fichier le plus court est presque certainement celui qui est le plus difficile à modifier en toute sécurité.
Ce qui compte, c’est la charge cognitive. Une question utile : combien de contexte un collègue a-t-il besoin avant de pouvoir modifier ce composant avec confiance ?
Symptômes indiquant que la complexité devient un frein
Faites attention à ces signaux :
- plusieurs appels non liés à
useEffect - une longue liste de variables d’état indépendantes
- des requêtes API effectuées directement depuis des composants UI
- des règles métier cachées à l’intérieur des gestionnaires de clic
- un rendu conditionnel fortement imbriqué
- une logique de permissions mêlée à la présentation
- la même transformation de données répétée en plusieurs endroits
- des responsabilités différentes pour lire et écrire la même état
- de longues chaînes de props transmises à travers des composants intermédiaires
- des requêtes réseau redondantes
- des tests qui doivent charger toute une page pour vérifier une petite règle
Le symptôme le plus révélateur est de nature sociale plutôt que technique : les gens évitent ce fichier. Dès que l’expression « ne touchez pas à ça, ça casse toujours » devient un adage au sein de l’équipe, le composant n’est plus une question de style mais un risque technique.
Définir les responsabilités avant de modifier le code
La première étape n’est pas d’extraire du JSX dans l’éditeur. Il s’agit d’écrire ce que fait réellement le composant. Pour l’écran de gestion du produit, l’inventaire pourrait ressembler à ceci :
ProductManagement
│
├── Fetch products
├── Transform API response
├── Filter products
├── Sort products
├── Manage selection
├── Validate permissions
├── Export data
├── Open/close dialogs
├── Handle errors
└── Render UI
Cette liste montre déjà que le composant remplit plusieurs rôles en même temps. Classées par type, les responsabilités se répartissent en données, comportement et présentation :
ProductManagement
│
┌─────────────┼─────────────┐
│ │ │
▼ ▼ ▼
Data Behavior Presentation
│ │ │
▼ ▼ ▼
API Filters/Rules Table
State Selection Dialog
Validation Actions
C’est avec cette classification que commence une bonne refonte. Il s’agit d’identifier les limites, et seulement ensuite de déterminer les fichiers concernés.
L’objectif : des couches avec une responsabilité claire
Diviser un grand fichier en cinq fichiers plus petits modifie l’organisation sur le disque mais pas la conception :
BigComponent.tsx
↓
5 smaller .tsx files
Le véritable objectif est une structure en couches où l’interface utilisateur, le comportement, la logique métier et l’accès aux données ont chacun leur place :
ProductPage
│
├─────────────── UI
│
├─────────────── Hooks / Behavior
│
├─────────────── Domain Logic
│
└─────────────── Data Access
En pratique, un dossier orienté fonction est un moyen pratique d’exprimer cela :
features/products/
│
├── components/
│ ├── ProductFilters.tsx
│ ├── ProductTable.tsx
│ ├── ProductActions.tsx
│ └── ProductDialog.tsx
│
├── hooks/
│ ├── useProducts.ts
│ ├── useProductFilters.ts
│ └── useProductSelection.ts
│
├── services/
│ └── productService.ts
│
├── utils/
│ ├── productMapper.ts
│ └── productValidator.ts
│
└── types.ts
Les noms des dossiers importent moins que ce que contient chaque module :
ProductTableest responsable de l’affichage du tableau.useProductFiltersgère le comportement des filtres.useProductsexpose les données des produits à React.productServicecommunique avec le backend.productValidatorcontient des règles de validation pures.
La structure souhaitée est un flux unidirectionnel, où chaque couche ne dépend que de celle qui se trouve en dessous d’elle :
UI
↓
Behavior
↓
Data Access
↓
Backend
et non un système où chaque aspect interfère avec tous les autres :
UI
↕
State
↕
API
↕
Business Logic
↕
UI
Pour en savoir plus sur l’aspect lié au « prop-drilling » dans ce problème, consultez comment réduire React prop drilling et les god components.
Étape un : extraire la logique pure hors de React
Les fonctions pures représentent l’approche la moins risquée pour l’extraction, ce qui en fait le point de départ naturel. Un outil d’aide au statut comme celui-ci est généralement défini à l’intérieur du composant :
const getProductStatus = (product: Product) => {
if (!product.active) {
return "Inactive";
}
if (product.stock <= 0) {
return "Out of Stock";
}
return "Available";
};
Rien à l’intérieur ne dépend de React : pas de useState, pas de useEffect, pas de Redux, pas de context. Il n’y a aucune raison pour que le composant en soit responsable, donc déplacez-le dans son propre module :
utils/productStatus.ts
Une fois qu’il se trouve là, la règle peut être testée directement, sans avoir à afficher quoi que ce soit :
describe("getProductStatus", () => {
it("returns inactive for disabled products", () => {
expect(
getProductStatus({
active: false,
stock: 10
})
).toBe("Inactive");
});
});
C’est une amélioration significative avec un risque très faible : la règle métier est désormais déconnectée de l’affichage, et un test échoué cible directement la règle plutôt qu’une page. Le principe directeur est simple : si une logique n’a pas besoin de React, React ne doit pas en être responsable.
Deuxième étape : extraire le comportement React dans des hooks
Viennent ensuite les éléments qui relèvent véritablement de React : l’état et les effets qui fonctionnent ensemble. Le chargement des produits correspond typiquement à un ensemble de trois variables d’état et d’un effet :
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetchProducts()
.then(setProducts)
.catch(setError)
.finally(() => setLoading(false));
}, []);
Transféré mot pour mot dans un hook personnalisé, il devient une seule unité avec une petite valeur de retour :
function useProducts() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetchProducts()
.then(setProducts)
.catch(setError)
.finally(() => setLoading(false));
}, []);
return {
products,
loading,
error
};
}
L’écran l’utilise alors en une seule ligne :
function ProductManagement() {
const {
products,
loading,
error
} = useProducts();
// composition
}
Le composant contient désormais moins de détails d’implémentation et ressemble davantage à une description de ce dont est composée l’écran. Le fait de copier le code mot pour mot est intentionnel : l’extraction ne doit pas modifier le comportement. Des améliorations telles que la mise à true de loading au départ (afin que la première affichage ne paraisse pas brièvement « chargé »), l’ignorance des réponses après désinstallation, ou l’adoption d’une bibliothèque de gestion de l’état serveur devraient faire l’objet d’une modification distincte et ultérieure.
Tout ne doit pas devenir un hook
C’est là que les refacturations tournent à l’over-engineering. Certaines bases de code finissent par disposer d’un hook pour chaque petite fonction :
useProductName()
useProductStatus()
useProductLabel()
useProductColor()
useProductFormatter()
Cela ne constitue pas une amélioration architecturale. Un hook personnalisé trouve son utilité lorsqu’il encapsule le comportement de React : l’état, les effets, le contexte, d’autres hooks, les abonnements ou les aspects liés au cycle de vie. Si tout ce dont on a besoin, c’est d’un formatteur,
formatProductName(product)
Alors, un appel de fonction simple est tout à fait approprié :
formatProductName(product)
En l’enveloppant dans un hook,
useProductName(product)
on applique les règles relatives aux hooks, on empêche son appel conditionnel ou à l’intérieur de boucles, sans aucun avantage. Les abstractions doivent réduire la charge mentale d’un codebase, et non l’augmenter.
Décider qui est responsable de chaque élément d’état
Le lieu où se trouve l’état est l’une des décisions les plus importantes lors d’un refactoring. Une classification utile distingue les données serveur, l’état global de l’interface utilisateur et l’état local de l’interface utilisateur :
Application State
│
├── Server State
│ ├── Products
│ ├── Orders
│ └── Customers
│
├── Global UI State
│ ├── Authentication
│ ├── Theme
│ └── Application Preferences
│
└── Local UI State
├── Modal
├── Selected Tab
└── Temporary Form State
Aucun extrême ne fonctionne : mettre tout dans Redux crée un stockage global encombré, tandis que garder tout local entraîne des doublons et des chaînes de propriétés. Pour chaque élément d’état, répondez à :
- Quels composants le lisent ?
- Pendant combien de temps doit-il exister ?
- Est-il géré par le serveur ou par le client ?
- Doit-il survivre aux navigations ?
Une heuristique fiable : conserver l’état le plus près possible de ses consommateurs, mais pas plus près que ne le permettent leurs besoins communs.
Éviter de créer un "parent divin"
Séparer un composant peut engendrer un nouveau problème. Les enfants sont de petite taille, mais le parent contient désormais tout l’état et le transmet :
<ProductPage
products={products}
filters={filters}
setFilters={setFilters}
selected={selected}
setSelected={setSelected}
permissions={permissions}
loading={loading}
error={error}
onExport={handleExport}
onDelete={handleDelete}
onEdit={handleEdit}
/>
Le fichier est plus court, mais le couplage reste identique ; il a simplement été déplacé d’un niveau. Avant de recourir à un contexte ou à un stockage pour cacher ces propriétés, demandez-vous si l’état est réellement partagé ou si les limites du composant ne sont pas mal définies. Le contexte est approprié lorsque plusieurs descendants ont réellement besoin de la même valeur, tandis qu’un stockage global convient lorsque l’état appartient à toute l’application. Aucun de ces approches ne doit masquer des limites qui ne correspondent pas au domaine concerné.
Remplacer progressivement le composant avec le pattern Strangler Fig
Réécrire l’ensemble du composant d’un coup est l’option la plus risquée. Le remplacer petit à petit est bien plus sûr. Au début, l’ancien composant fait tout :
BigComponent
└── Everything
Puis une fonctionnalité, comme le filtrage, est transférée dans son propre composant tandis que le reste reste inchangé :
BigComponent
├── ProductFilters
└── Everything else
La table est traitée lors de l’itération suivante :
BigComponent
├── ProductFilters
├── ProductTable
└── Everything else
Finalement, la page n’est plus qu’une composition pure, et l’implémentation ancienne a disparu :
ProductPage
├── ProductFilters
├── ProductTable
├── ProductActions
└── ProductDialog
C’est le pattern Strangler Fig, généralement décrit pour migrer des systèmes backend, appliqué au frontend : de nouvelles parties se développent autour de l’implémentation ancienne, chacune prenant en charge une fonctionnalité tandis que le reste de l’application continue de fonctionner. L’avantage est que la production n’a jamais besoin de passer
100% Legacy
directement à
100% New
dans une seule version. Chaque état intermédiaire est prêt à être déployé et réversible.
Préserver la stabilité du contrat public
Dans les grandes organisations, un composant de ce type est souvent affiché par plusieurs écrans utilisant un ensemble spécifique de propriétés :
<ProductManagement
clientId={clientId}
onSelect={handleSelect}
/>
Conserver cette interface inchangée tout au long du refacteurage. Derrière elle, vous êtes libre de la réorganiser complètement :
Existing Public API
│
▼
ProductManagement
│
┌────┼────┐
▼ ▼ ▼
Hooks UI Services
Les utilisateurs n’ont jamais besoin de savoir que les éléments internes ont été déplacés vers des hooks, des composants UI et des services, ce qui limite l’impact de chaque modification. Cette discipline est particulièrement importante pour les bibliothèques de composants partagés, les systèmes de conception, les micro-frontends, les applications d’entreprise et tout ce qui est utilisé par plusieurs équipes. Si l’API doit être modifiée, procédez à une migration distincte et explicite.
Détacher les effets qui cachent plusieurs responsabilités
Les composants volumineux contiennent souvent des effets qui effectuent plusieurs tâches non liées entre elles :
useEffect(() => {
fetchData();
updateAnalytics();
validatePermissions();
calculateSomething();
}, [dependency]);
Le problème ne réside pas dans le nombre d’instructions, mais plutôt dans le fait qu’elles représentent probablement quatre responsabilités distinctes, chacune ayant ses propres dépendances réelles. Séparez-les afin que chaque effet indique clairement à quoi il réagit :
useEffect(() => {
fetchData();
}, [dependency]);
useEffect(() => {
updateAnalytics();
}, [analyticsDependency]);
Pendant que vous y êtes, cherchez les effets qui se contentent de calculer des valeurs. Stocker une liste filtrée dans l’état et la synchroniser depuis un effet entraîne un rendu supplémentaire et peut provoquer l’utilisation de données obsolètes :
const [filteredProducts, setFilteredProducts] = useState([]);
useEffect(() => {
setFilteredProducts(
products.filter(product => product.active)
);
}, [products]);
Le calculer pendant le rendu est plus simple et garantit toujours une cohérence :
const filteredProducts = products.filter(
product => product.active
);
Ne mettez en cache ou ne synchronisez que s’il y a une raison réelle, comme un calcul coûteux mesuré (où useMemo est l’outil approprié, et non un effet).
Séparez l’état serveur de l’état UI
Dans les écrans plus grands, distinguer ces deux catégories apporte rapidement des avantages :
ProductPage
│
┌───────────┴───────────┐
│ │
Server State UI State
│ │
┌─────┴─────┐ ┌──────┴──────┐
│ │ │ │
products loading filter modal
error status selected tab
La liste des produits et le fait qu’un dialogue soit ouvert représentent fondamentalement deux types de données différents. Le backend gère les produits ; le navigateur gère le dialogue. Les données serveur nécessitent du cache, un rechargement, un chargement dynamique ainsi qu’un traitement des erreurs, tandis que l’état de l’interface utilisateur est éphémère et local. Mélanger ces éléments dans une seule propriété useState rend les composants plus difficiles à comprendre. La manière dont cela se passe avec des outils dédiés est abordée dans React Query et Redux : repenser l’état serveur dans les grandes applications.
Un flux de travail de refactoring, pas une réécriture
Un refactoring en production est un processus itératif :
Identify
↓
Understand
↓
Protect
↓
Extract
↓
Test
↓
Validate
↓
Deploy
↓
Measure
↓
Repeat
Ce n’est pas cela :
Delete component
↓
Rewrite everything
↓
Hope production works
C’est ce contraste qui distingue le refactoring d’un système existant de la création d’un projet à partir de zéro.
À quoi ressemble le flux de données résultant
Lorsque les responsabilités sont séparées, une interaction utilisateur passe par des couches bien définies avant de revenir :
User
│
▼
ProductPage
│
▼
useProductFilters
│
▼
useProducts
│
▼
ProductService
│
▼
API
│
▼
Backend
│
▼
ProductService
│
▼
useProducts
│
▼
ProductTable
│
▼
Updated UI
En résumé, le principe repose sur une ligne de dépendance directe :
Presentation
↓
Behavior
↓
Data Access
↓
Backend
La table ne doit pas savoir comment fonctionne l’authentification, le service API ne doit pas connaître la manière dont la table est affichée, et une règle métier ne doit pas se soucier du fait que l’interface utilise un modal ou un drawer. C’est là la séparation des préoccupations, avec un objectif concret : chaque modification reste confinée à une seule couche.
Faites attention aux requêtes API redondantes
Cette régression est assez fréquente pour mériter une mise en garde spécifique. Avant le refactoring, un composant récupérait les données :
ProductPage
│
└── fetchProducts()
Après l’extraction, à la fois la page et un composant enfant appellent le même mécanisme de récupération des données :
ProductPage
│
├── useProducts()
│
└── ProductTable
│
└── useProducts()
Le code semble plus ordonné, mais un simple hook fetch-in-effect s’exécute désormais deux fois, ce qui double le nombre de requêtes et peut afficher des snapshots différents pour les deux composants. Chaque fois que vous déplacez la logique de récupération des données, demandez à qui appartient cet état serveur. Il doit y avoir une seule source de vérité. Si plusieurs composants ont besoin des mêmes données distantes, récupérez-les une seule fois et transmettez-les, ou utilisez une couche d’état serveur avec mise en cache qui élimine les doublons de requêtes, plutôt que de laisser chaque consommateur effectuer des récupérations indépendamment.
Les tests sont le filet de sécurité
Au préalable de modifier un composant critique, énumérez les comportements sur lesquels dépend l’application :
Product Management
│
├── Filter products
├── Select products
├── Edit product
├── Delete product
├── Export products
└── Permission handling
Puis protégez-les au niveau adéquat :
- Les tests unitaires conviennent à la validation, aux transformations, aux règles métier, au formatage et au mappage.
- Les tests de composants conviennent à l’affichage, aux interactions, ainsi qu’aux états de chargement et d’erreur.
Un refactoring ne nécessite pas une couverture de 100 %. Il exige plutôt une confiance dans le comportement qui compte. Les tests écrits en fonction du comportement actuel avant de commencer sont les plus précieux, car ils prouvent que rien n’a changé.
Les petits pull requests font partie de la conception
Le coût de l’examen et le coût du retrait s’accentuent avec la taille d’une modification. Comparez une série comme celle-ci :
Extract ProductFilters
suivi de
Extract ProductTable
et puis
Extract product-fetching behavior
avec une seule modification intitulée
Refactor ProductManagement
qui touche
4,000 lines changed
Les petits PR sont plus faciles à examiner et à annuler, réduisent le risque de regressions, permettent de conserver une histoire Git lisible, simplifient le débogage et diminuent les conflits de fusion. Pour un chef technique, il s’agit moins d’une préférence en matière de qualité du code que d’un moyen de permettre à l’équipe de progresser en parallèle.
Conserver le refactoring séparé des autres modifications
Résistez à l’envie d’associer un refactoring à tout le reste de la liste des demandes :
React upgrade
+
State migration
+
API changes
+
UI redesign
+
CSS migration
+
Component refactoring
Si la production tombe en panne après une telle mise à jour, il est difficile de trouver la cause car n’importe l’une des six modifications pourrait en être responsable. Triez-les plutôt par ordre :
Refactor
↓
Stabilize
↓
Measure
↓
Optimize
Appliquer une seule modification significative à la fois permet de faire en sorte que chaque regression soit attribuable.
Défis fonctionnels pour les remplacements à haut risque
Pour les workflows critiques, un défi fonctionnel ajoute une couche de sécurité en permettant à deux implémentations de coexister :
return useNewProductExperience
? <NewProductManagement />
: <LegacyProductManagement />;
Vous pouvez alors augmenter progressivement l’exposition tout en surveillant les taux d’erreur et les indicateurs métier :
1%
↓
5%
↓
25%
↓
50%
↓
100%
Cela est particulièrement utile pour les écrans à fort trafic, les flux critiques pour les revenus, les applications d’entreprise, les grands micro-frontends et les changements architecturaux à risque. Traitez ce drapeau comme temporaire : une fois que le nouveau chemin s’est avéré fiable, supprimez l’implémentation ancienne ainsi que le drapeau, sinon vous finirez par devoir gérer les deux.
Auparavant et après
Le point de départ, condensé, regroupe les données, le filtrage, la sélection, l’export et les dialogues dans une seule fonction :
function ProductManagement() {
const [products, setProducts] = useState([]);
const [filter, setFilter] = useState("");
const [selected, setSelected] = useState([]);
const [loading, setLoading] = useState(false);
const [showDialog, setShowDialog] = useState(false);
useEffect(() => {
// Fetch products
}, []);
const filteredProducts = products.filter(...);
const handleSelect = (...) => {
// Selection logic
};
const handleExport = (...) => {
// Export logic
};
return (
<>
{/* hundreds of lines of JSX */}
</>
);
}
Rien de tout cela n’est nécessairement incorrect. Le problème est qu’un seul composant est responsable de trop de choses. Après le refactoring, la page relie des hooks aux composants présentationnels :
function ProductManagement() {
const {
products,
loading
} = useProducts();
const {
filter,
setFilter,
filteredProducts
} = useProductFilters(products);
const {
selected,
handleSelect
} = useProductSelection();
return (
<ProductPage>
<ProductFilters
value={filter}
onChange={setFilter}
/>
<ProductTable
products={filteredProducts}
selected={selected}
onSelect={handleSelect}
loading={loading}
/>
<ProductActions
products={selected}
/>
</ProductPage>
);
}
La page décrit maintenant la composition, qui représente l’état le plus sain pour un composant au niveau de page. Chaque hook et chaque enfant peut être lu, testé et modifié indépendamment.
Plus petit ne signifie pas plus rapide
Il est tentant de penser que transformer
1 large component
en
10 smaller components
améliore les performances. Cela n’est pas le cas en soi, et cela peut même aggraver les choses en introduisant des rendus supplémentaires, des calculs répétés, des mises à jour de contexte perturbatrices, des requêtes dupliquées, des useMemo et useCallback inutiles, ainsi que des props dont l’identité change à chaque rendu. L’architecture et les performances sont liées mais relèvent de domaines distincts. Mesurez avant d’optimiser, et utilisez le profilage ainsi que les données de telemétrie en environnement de production pour confirmer qu’il y a bien eu une amélioration.
Jugez le succès en fonction du coût de la prochaine modification
Une diminution du nombre de lignes est un effet secondaire agréable :
1,200 lines → 300 lines
Mais ce n’est pas l’objectif. Comparez la composante avant et après en termes de dimensions telles que :
- la complexité cognitive et le couplage, qui devraient diminuer
- la testabilité, qui devrait augmenter
- le nombre d’appels API redondants et le nombre d’erreurs en production
- les temps de rendu en millisecondes
- le taux de régressions après les mises à jour
- l’effort requis pour une modification typique
Les directions à viser :
Complexity ↓
Coupling ↓
Regression risk ↓
Testability ↑
Maintainability ↑
Developer speed ↑
Le résultat le plus précieux est un coût de modification réduit, et non moins de lignes de code.
Quand ne pas refactorer
Tout composant volumineux ne mérite pas cet effort. Les raisons insuffisantes incluent : le fichier est long, certaines personnes préfèrent des fichiers petits, une nouvelle architecture est à la mode, vous souhaitez introduire une autre bibliothèque, ou le code ne correspond pas aux goûts personnels. Si le composant est stable, bien testé, rarement modifié, à faible risque et isolé, une grande refonte peut ajouter plus de risques qu’elle n’en élimine. La décision repose sur un compromis :
Expected future benefit
│
▼
Is it worth the
refactoring risk?
│
Yes / No
C’est un jugement d’ingénieur concernant les changements futurs attendus, et non une préférence de formatage.
Comprendre le comportement existant est la partie difficile
Rédiger la nouvelle implémentation n’est que rarement la partie difficile. La véritable difficulté réside dans la compréhension suffisamment approfondie du comportement actuel afin de préserver ce sur quoi comptent les utilisateurs. Les composants en production contiennent des connaissances qui ne sont pas évidentes à partir de leur lecture : hypothèses non écrites concernant le produit, règles d’accès, liens vers des systèmes plus anciens, réponses du backend étranges qu’ils tolèrent, événements de suivi sur lesquels comptent des utilisateurs ultérieurs, solutions de contournement pour certains navigateurs, appels provenant d’entités oubliées, correctifs issus d’incidents passés ainsi que les attentes des autres équipes. Ce savoir caché explique pourquoi une réécriture complète est bien plus risquée qu’une décomposition progressive. L’ordre de priorité à garder en tête : préserver le comportement, puis améliorer la structure, enfin optimiser.
Checklist de refactoring pour la production
Au préalable :
- Identifier tous les utilisateurs du composant.
- Dokumenter son comportement actuel et définir ses responsabilités.
Lors du refactoring :
- Préserver la stabilité du contrat public.
- Extraire d’abord les fonctions pures, puis le comportement React.
- Conserver l’état près de ses consommateurs et éviter les abstractions qui ne rapportent rien.
- Exclure les modifications non pertinentes, surveiller les requêtes dupliquées et garder les PR de petite taille.
Au moment du merge :
- Vérification de types, tests unitaires, de composants et end-to-end, ainsi que le processus de build : tout doit réussir.
- Aucune requête dupliquée et aucune régression inattendue dans le rendu.
Après déploiement :
- Surveiller les erreurs, le trafic API et les performances, et comparer avec les valeurs de base.
- Veiller sur les flux de travail critiques des utilisateurs.
- Supprimez le code obsolète une fois que le nouveau chemin aura gagné en fiabilité.
Conclusion
Les grands composants React ne sont pas l’ennemi ; c’est la complexité non maîtrisée. La stratégie perdante se présente comme suit :
Big Component
↓
Rewrite Everything
La stratégie gagnante consiste en une boucle patiente qui mène à une architecture plus simple :
Big Component
↓
Understand
↓
Protect
↓
Extract
↓
Test
↓
Measure
↓
Repeat
↓
Simpler Architecture
L’objectif n’est pas d’avoir plus de fichiers, mais des modifications plus sûres : des versions qui ne surprennent personne, et une base de code que le prochain développeur pourra modifier dans six mois sans crainte.