Architecture de pile modale React pour apps évolutives
Dans les applications React complexes, les modales deviennent vite un casse-tête. Une modale simple avec useState fonctionne isolément, mais à mesure que le projet grandit, vous finissez avec des composants dupliqués, du prop drilling et des dépendances d'état emmêlées. La solution ? Une pile de superpositions typée qui sépare la logique par source de déclenchement.
Prenons un tableau de bord d'administration de marketplace typique avec un tableau de produits. Chaque ligne nécessite une modale pour ajouter des identifiants (IMEI, SGTIN), avec tableaux de commandes, validation, imports Excel et un assistant multi-étapes.
Problèmes du rendu local
Rendre un IdentificationModal dans chaque ligne de tableau crée des milliers d'éléments DOM cachés. Le composant est bourré de tableaux, d'entrées masquées, de hooks RTK Query et d'un stepper — la consommation mémoire explose.
Voici un mauvais exemple :
const ProductRow = ({ product }) => {
const [isOpen, setIsOpen] = useState(false);
return (
<tr>
<td>{product.name}</td>
<td>
<Button onClick={() => setIsOpen(true)}>Ajouter identifiants</Button>
<IdentificationModal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
productId={product.id}
/>
</td>
</tr>
);
};
Leçon clé : Les modales doivent être rendues une seule fois au niveau de la page.
Prop drilling et état global
Déplacer la modale vers le haut de l'arbre corrige le gonflement DOM, mais oblige à passer des callbacks à travers 4-5 couches de composants :
ProductsPage→ProductsTable→TableBody→ProductRow→ActionsMenu
Redux avec des dispatchs openIdent(productId) élimine les props, mais devient chaotique avec plusieurs contextes.
Multiples sources de données
La modale puise dans trois endroits avec des API différentes :
| Source de déclenchement | Commandes | Chargement données | Modèle Excel |
|--------------------------|-----------|--------------------|--------------|
| En-tête de tableau | Toutes les commandes | ordersApi.getIdentOrders() | ordersApi.getExcel() |
| Ligne de tableau | Une commande | Filtre par orderId | ordersApi.getExcel() |
| Panier | Commandes du panier | cartsApi.getCartIdentOrders(cartId) | cartsApi.getExcel() |
Entasser la logique dans la modale en fait un God Object plein de if, d'invalidation de cache et d'analytique. L'alternative — une modale UI pure avec props orders et onSave — échoue car la page ignore le contexte à l'avance.
Dépendances des modales
Ouvrir la modale d'identifiant masque le panneau d'actions groupées ; la fermer le restaure. Ajouter une modale de confirmation (« Modifications non sauvegardées ? ») oblige à jongler manuellement avec des flags :
openConfirmClose: (state) => {
state.isConfirmOpen = true;
state.isIdentOpen = false;
},
cancelConfirmClose: (state) => {
state.isConfirmOpen = false;
state.isIdentOpen = true;
},
Des structures de flags plats ne capturent ni l'ordre d'empilement ni les stratégies de rollback.
Architecture de solution : Pile typée
Ce système résout trois problèmes d'un coup :
- Rendu unique via une pile Redux avec file d'attente de superpositions.
- Séparation des contextes par wrappers de sources.
- Gestion automatique des dépendances avec règles de pile.
Slice Redux pour la pile
export enum EOverlayName {
BulkActions = 'bulkActions',
IdentFromHeader = 'identFromHeader',
IdentFromRow = 'identFromRow',
IdentFromCart = 'identFromCart',
EditProduct = 'editProduct',
// ...
}
type OverlayItem<TData = unknown> = {
name: EOverlayName;
data: TData;
priority?: number;
};
type OverlayStack = OverlayItem[];
La pile stocke des données typées et l'ordre. Les actions asynchrones poussent des superpositions avec payloads.
Hooks de superposition pour déclencheurs
const useOverlay = () => {
const dispatch = useDispatch();
const open = <TData>(name: EOverlayName, data: TData) => {
dispatch(openOverlay({ name, data }));
};
const close = (name: EOverlayName) => {
dispatch(closeOverlay(name));
};
return { open, close };
};
Bouton de ligne : open('identFromRow', { productId, orderId }). La modale lit directement dans la pile — fini le prop drilling.
Wrappers de sources pour contextes
Chaque source exporte un adaptateur :
// identFromRow.adapter.ts
export const useIdentFromRowAdapter = () => {
const { open } = useOverlay();
const openIdent = (productId: string, orderId: string) => {
const orders = ordersApi.useGetIdentOrdersQuery();
const filtered = orders.data?.filter(o => o.id === orderId);
open('identFromRow', { orders: filtered, productId });
};
return { openIdent };
};
La modale reçoit des orders préparées et onSave sans connaître la source.
Stratégies d'ouverture/fermeture
La pile impose des règles :
- Remplacement : Ouvrir
identfermebulkActions. - Pause : Fermer reprend la superposition précédente.
- Effacement : Confirmation réinitialise la pile.
const openOverlay = createAsyncThunk(
'overlay/open',
async ({ name, data }, { getState }) => {
const stack = selectStack(getState());
// Logique de remplacement/priorité
return { name, data };
}
);
Avantages clés
- La pile élimine prop drilling, duplication DOM et dépendances d'état avec une seule architecture.
- Les wrappers de sources isolent la logique API ; les modales restent UI pures.
- Le typage
OverlayItem<TData>garantit la sécurité des données. - Les stratégies asynchrones automatisent l'ordre d'ouverture/fermeture.
- Scalable à des dizaines de modales sans alourdir le reducer.
— Editorial Team
Aucun commentaire pour le moment.