Le spread et le piège de la copie superficielle
Copier un tableau ou un objet avec le spread, comprendre pourquoi les objets imbriqués restent partagés, et produire des mises à jour d’état réellement immuables.
Où en êtes-vous avec cette notion ?
Une indication personnelle, enregistrée uniquement dans ce navigateur.
À lire ensuite
Le format café
L’essentiel à comprendre, le temps d’un café.
Envie de creuser ? Un cours plus complet vous attend juste après, à déplier sans quitter cette page.
Ce que vous allez comprendre
L’opérateur ... déplie le contenu d’un tableau ou d’un objet ailleurs. Il sert à copier, ajouter et fusionner sans modifier l’original — mais sur un seul niveau de profondeur.
Déplier un tableau
// Node.js ou console du navigateur.
const fruits = ['pomme', 'poire'];
const copie = [...fruits];
copie.push('banane');
console.log(copie); // [ 'pomme', 'poire', 'banane' ]
console.log(fruits); // [ 'pomme', 'poire' ] : intact
const nouveauxFruits = [...fruits, 'banane'];
['pomme', 'poire']
↓ ...fruits
'pomme', 'poire'
Cette forme est omniprésente en React : setUsers([...users, newUser]) crée un nouveau tableau au lieu de modifier l’ancien.
Déplier un objet, et l’ordre des clés
const user = { name: 'Jean', role: 'user' };
const admin = { ...user, role: 'admin' };
const inverse = { role: 'admin', ...user };
console.log(admin); // { name: 'Jean', role: 'admin' }
console.log(inverse); // { name: 'Jean', role: 'user' }
console.log(user); // { name: 'Jean', role: 'user' } : intact
L’objet se construit de gauche à droite et la dernière valeur écrite gagne. Dans inverse, le role du spread écrase celui écrit avant lui. Ce détail d’ordre provoque régulièrement des bugs silencieux.
Le piège : la copie reste superficielle
const utilisateur = { nom: 'Jean', adresse: { ville: 'Montpellier' } };
const copieUtilisateur = { ...utilisateur };
copieUtilisateur.adresse.ville = 'Nîmes';
console.log(utilisateur.adresse.ville); // 'Nîmes'
utilisateur ─────► objet A
copieUtilisateur ─────► objet B (bien deux objets distincts)
utilisateur.adresse ─────┐
├──► le MÊME objet adresse
copieUtilisateur.adresse ─────┘
L’objet extérieur est neuf ; l’objet adresse est seulement recopié par référence. Pour rendre ce niveau indépendant, il faut le déplier lui aussi :
const copieSure = { ...utilisateur, adresse: { ...utilisateur.adresse } };
Fiche mémo
[...tableau]et{ ...objet }créent une nouvelle référence de premier niveau.- Les objets et tableaux imbriqués restent partagés.
- Dans un objet, la dernière clé écrite l’emporte.
- Pour modifier en profondeur, dépliez chaque niveau du chemin concerné.
L’idée à emporter : ... copie une couche, pas une structure ; tout ce qui est plus profond reste commun à l’original et à la copie.
Et si on allait plus loin ?
Le café vous a donné les repères. Prenez maintenant le temps de comprendre les mécanismes et de pratiquer, si vous le souhaitez.
Aller plus loinDe la copie superficielle à l’état React qui se met vraiment à jourDévelopper le coursReplier le cours
Ce que vous saurez faire
Copier en profondeur un objet imbriqué, savoir où structuredClone() s’arrête, et expliquer pourquoi l’immutabilité permet à React de détecter un changement. Les exemples se lisent avec Node.js ou dans la console ; les extraits React supposent un projet déjà configuré.
1. Copier en profondeur à la main, niveau par niveau
Le spread ne copie qu’une couche. Pour rendre une valeur enfouie indépendante, dépliez chaque niveau du chemin qui y mène.
// Node.js ou console du navigateur.
const etat = {
utilisateur: { nom: 'Jean', preferences: { theme: 'clair' } },
tags: ['javascript'],
};
const suivant = {
...etat,
utilisateur: {
...etat.utilisateur,
preferences: { ...etat.utilisateur.preferences, theme: 'sombre' },
},
};
console.log(suivant.utilisateur.preferences.theme); // 'sombre'
console.log(etat.utilisateur.preferences.theme); // 'clair'
console.log(suivant.tags === etat.tags); // true
Le dernier console.log n’est pas une erreur : tags n’étant pas sur le chemin modifié reste volontairement partagé — c’est le partage structurel, sûr tant que personne ne modifie ce tableau en place.
Référence : syntaxe de décomposition.
2. structuredClone() : une copie profonde intégrée
Une copie profonde est disponible sans bibliothèque, dans les navigateurs récents et dans Node.js à partir de la version 17.
const etat = { utilisateur: { nom: 'Jean', preferences: { theme: 'clair' } } };
const clone = structuredClone(etat);
clone.utilisateur.preferences.theme = 'sombre';
console.log(etat.utilisateur.preferences.theme); // 'clair' : vraiment indépendant
Elle gère les objets imbriqués, les Date, Map, Set et les références circulaires — que JSON.parse(JSON.stringify(x)) casse. Ses limites sont réelles :
structuredClone({ action: () => {} }); // DataCloneError : fonctions non clonables
structuredClone(document.body); // DataCloneError : nœuds DOM non plus (navigateur)
class Utilisateur {
constructor(nom) { this.nom = nom; }
saluer() { return 'Salut ' + this.nom; }
}
const copie = structuredClone(new Utilisateur('Jean'));
console.log(copie.nom, copie instanceof Utilisateur, typeof copie.saluer);
// Jean false undefined
Une instance de classe revient donc en objet ordinaire : les données survivent, le prototype et les méthodes disparaissent. Enfin, cette fonction recopie toute la structure : quand une seule branche change, la copie ciblée du point 1 reste généralement préférable.
Référence : structuredClone().
3. Pourquoi l’immutabilité aide React à voir le changement
React ne compare pas le contenu de votre état : il compare des références, avec une comparaison de type Object.is. C’est très rapide, quelle que soit la taille des données — mais aveugle si la référence n’a pas bougé.
// À éviter : la référence ne change pas.
user.name = 'Pierre';
setUser(user); // React compare user avec user : identique, souvent aucun rendu
// Préféré : nouvel objet, donc nouvelle référence.
setUser({ ...user, name: 'Pierre' });
La règle vaut aussi en profondeur : si user.adresse reste le même objet, un composant enfant mémorisé qui reçoit cette adresse ne se mettra pas à jour. D’où l’écriture complète :
setUser({ ...user, adresse: { ...user.adresse, ville: 'Nîmes' } });
React.memo et les tableaux de dépendances de useEffect et useMemo reposent sur le même mécanisme : créer un nouvel objet rend ces comparaisons fiables, muter en place les rend fausses.
Pièges et limites
- L’ordre des clés décide.
{ role: 'admin', ...user }laisse gagneruser.role. Placez après le spread ce qui doit l’emporter. - Les tableaux sont concernés autant que les objets.
[...utilisateurs]duplique le tableau, pas les objets qu’il contient :copie[0].nom = 'X'se voit dans l’original. - Le spread perd le prototype.
{ ...instance }produit un objet ordinaire : plus de méthodes de classe, et seules les propriétés propres énumérables sont reprises. structuredClone()a ses refus. Fonctions et nœuds DOM lèvent uneDataCloneError; les instances de classe perdent leur prototype.JSON.parse(JSON.stringify(x))n’est pas un substitut. Il perdundefined, change lesDateen chaînes et échoue sur les cycles.
À vous de jouer
Voici un état d’application React :
const etat = {
utilisateur: { nom: 'Jean', preferences: { theme: 'clair', langue: 'fr' } },
tags: ['javascript'],
};
Écrivez majTheme(etat, theme), qui renvoie un nouvel état où preferences.theme prend la valeur demandée et où 'react' est ajouté à tags. Ni etat ni ses objets imbriqués ne doivent être modifiés, et preferences.langue doit être conservée. Vérifiez avec des console.log.
Correction commentée
// Node.js ou console du navigateur.
function majTheme(etat, theme) {
return {
...etat,
utilisateur: {
...etat.utilisateur,
preferences: { ...etat.utilisateur.preferences, theme },
},
tags: [...etat.tags, 'react'],
};
}
const suivant = majTheme(etat, 'sombre');
console.log(suivant.utilisateur.preferences); // { theme: 'sombre', langue: 'fr' }
console.log(suivant.tags); // [ 'javascript', 'react' ]
console.log(etat.utilisateur.preferences); // { theme: 'clair', langue: 'fr' }
console.log(etat.tags); // [ 'javascript' ] : intact
console.log(suivant.utilisateur === etat.utilisateur); // false
La règle appliquée tient en une phrase : on déplie chaque niveau situé sur le chemin de la valeur modifiée. Le chemin est ici etat → utilisateur → preferences → theme, donc trois spreads emboîtés. Sauter l’un d’eux ferait réapparaître le piège de la copie superficielle : avec un simple { ...etat, ... }, l’objet preferences resterait partagé et changer le thème modifierait aussi l’état d’origine.
theme est écrit en abrégé parce que le paramètre porte déjà ce nom : { ...preferences, theme } équivaut à { ...preferences, theme: theme }. Sa position, après le spread, garantit qu’il écrase l’ancienne valeur, tandis que langue est conservée sans effort par ce même spread.
tags reçoit [...etat.tags, 'react'] plutôt que etat.tags.push('react'). push modifierait le tableau d’origine et renverrait un nombre, pas un tableau ; le spread crée la nouvelle référence que React comparera.
Les deux derniers console.log sont la vérification utile : ce sont exactement les comparaisons que React effectue pour décider d’un nouveau rendu. Une variante correcte mais plus coûteuse consisterait à cloner l’état entier avec structuredClone(etat) puis à le modifier en place — à condition qu’il ne contienne ni fonction ni instance de classe.
Vous pouvez aussi vous arrêter ici. L’approfondissement est facultatif.
Les repères Cours Café
Pour aller à la source
Documentation de référence. Vérification éditoriale encore à effectuer.
Gardez une trace de cette idée.
Favoris, notes et progression seront disponibles après connexion du stockage distant.