Keys
Que sont les clés ?
Les clés représentent des identités suivies par Mithril. Elles permettent à Mithril de différencier les vnodes et de gérer correctement leur état. Vous pouvez les ajouter aux vnodes d'éléments, de composants et de fragments via l'attribut key
. Voici un exemple d'utilisation :
m('.user', { key: user.id }, [
/* ... */
]);
Elles sont utiles dans les cas suivants :
- Gestion de l'état : Lorsque vous affichez des données issues d'un modèle ou d'autres données avec état, les clés sont nécessaires pour maintenir l'état local associé au bon sous-arbre.
- Animations CSS : Lorsque vous animez indépendamment plusieurs nœuds adjacents à l'aide de CSS et que vous pouvez supprimer l'un d'eux individuellement, les clés garantissent que les animations restent associées aux éléments et ne sautent pas de manière inattendue vers d'autres nœuds.
- Réinitialisation de sous-arbres : Lorsque vous devez réinitialiser un sous-arbre sur commande, ajoutez une clé, puis modifiez-la et redessinez à chaque fois que vous souhaitez le réinitialiser.
Restrictions relatives aux clés
Important : Pour tous les fragments, leurs enfants doivent contenir soit exclusivement des vnodes avec des attributs key
(fragment avec clé), soit exclusivement des vnodes sans attributs key
(fragment sans clé). Les attributs de clé ne peuvent exister que sur les vnodes qui supportent les attributs, à savoir les vnodes d'éléments, de composants et de fragments. Les autres vnodes, comme null
, undefined
et les chaînes de caractères, ne peuvent pas avoir d'attributs d'aucune sorte, ils ne peuvent donc pas avoir d'attributs key
et ne peuvent donc pas être utilisés dans les fragments avec clé.
Cela signifie que des exemples tels que [m(".foo", {key: 1}), null]
et ["foo", m(".bar", {key: 2})]
ne fonctionneront pas, mais [m(".foo", {key: 1}), m(".bar", {key: 2})]
et [m(".foo"), null]
fonctionneront. Si vous oubliez cette règle, vous recevrez un message d'erreur explicite.
Lier des données de modèle dans des listes de vues
Lorsque vous affichez des listes, en particulier des listes modifiables, vous manipulez souvent des éléments tels que des tâches à faire modifiables. Ces éléments ont un état et une identité, et vous devez fournir à Mithril les informations nécessaires pour les suivre.
Supposons que nous ayons une simple liste de publications sur les médias sociaux, où vous pouvez commenter et masquer les publications.
// `User` et `ComposeWindow` omis pour des raisons de concision.
function CommentCompose() {
return {
view: function (vnode) {
var post = vnode.attrs.post;
return m(ComposeWindow, {
placeholder: 'Écrivez votre commentaire...',
submit: function (text) {
return Model.addComment(post, text);
},
});
},
};
}
function Comment() {
return {
view: function (vnode) {
var comment = vnode.attrs.comment;
return m(
'.comment',
m(User, { user: comment.user }),
m('.comment-body', comment.text),
m(
'a.comment-hide',
{
onclick: function () {
Model.hideComment(comment).then(m.redraw);
},
},
"Je n'aime pas ça"
)
);
},
};
}
function PostCompose() {
return {
view: function (vnode) {
var comment = vnode.attrs.comment;
return m(ComposeWindow, {
placeholder: 'Écrivez votre publication...',
submit: Model.createPost,
});
},
};
}
function Post(vnode) {
var showComments = false;
var commentsFetched = false;
return {
view: function (vnode) {
var post = vnode.attrs.post;
var comments = showComments ? Model.getComments(post) : null;
return m(
'.post',
m(User, { user: post.user }),
m('.post-body', post.text),
m(
'.post-meta',
m(
'a.post-comment-count',
{
onclick: function () {
if (!showComments && !commentsFetched) {
commentsFetched = true;
Model.fetchComments(post).then(m.redraw);
}
showComments = !showComments;
},
},
post.commentCount,
' commentaire',
post.commentCount === 1 ? '' : 's'
),
m(
'a.post-hide',
{
onclick: function () {
Model.hidePost(post).then(m.redraw);
},
},
"Je n'aime pas ça"
)
),
showComments
? m(
'.post-comments',
comments == null
? m('.comment-list-loading', 'Chargement...')
: [
m(
'.comment-list',
comments.map(function (comment) {
return m(Comment, { comment: comment });
})
),
m(CommentCompose, { post: post }),
]
)
: null
);
},
};
}
function Feed() {
Model.fetchPosts().then(m.redraw);
return {
view: function () {
var posts = Model.getPosts();
return m(
'.feed',
m('h1', 'Feed'),
posts == null
? m('.post-list-loading', 'Chargement...')
: m(
'.post-view',
m(PostCompose),
m(
'.post-list',
posts.map(function (post) {
return m(Post, { post: post });
})
)
)
);
},
};
}
Comme vous pouvez le constater, ce code encapsule beaucoup de fonctionnalités. Concentrons-nous sur ces deux parties en particulier :
// Dans le composant `Feed`
m(
'.post-list',
posts.map(function (post) {
return m(Post, { post: post });
})
);
// Dans le composant `Post`
m(
'.comment-list',
comments.map(function (comment) {
return m(Comment, { comment: comment });
})
);
Chacune de ces parties fait référence à un sous-arbre avec un état associé, dont Mithril n'a aucune connaissance (Mithril ne connaît que les vnodes). Lorsque vous ne leur attribuez pas de clé, le comportement peut devenir étrange et inattendu. Dans ce cas, essayez de cliquer sur "N commentaires" pour afficher les commentaires, de taper du texte dans la zone de composition des commentaires en bas, puis de cliquer sur "Je n'aime pas ça" sur une publication au-dessus. Voici une démo en direct pour que vous puissiez l'essayer, avec un modèle simulé. (Remarque : si vous êtes sur Edge ou IE, vous pourriez rencontrer des problèmes liés à la longueur du hachage du lien.)
Au lieu de se comporter comme prévu, le comportement devient inattendu. Par exemple, la liste de commentaires que vous aviez ouverte se ferme, et la publication suivante affiche en permanence "Chargement...", même si les commentaires sont déjà censés être chargés. En effet, les commentaires sont chargés de manière différée et le code suppose que le même commentaire est transmis à chaque fois (ce qui semble relativement sain ici), mais dans ce cas, ce n'est pas le cas. Cela est dû à la façon dont Mithril patche itérativement les fragments sans clé, de manière très simple. Donc, dans ce cas, la différence pourrait ressembler à ceci :
- Avant :
A, B, C, D, E
- Patché :
A, B, C -> D, D -> E, E -> (supprimé)
Et comme le composant reste le même (c'est toujours Comment
), seuls les attributs changent et il n'est pas remplacé.
Pour corriger ce problème, il suffit d'ajouter une clé, afin que Mithril sache qu'il doit potentiellement déplacer l'état si nécessaire pour résoudre ce problème. Voici un exemple en direct et fonctionnel avec tous les correctifs appliqués.
// In the `Feed` component
m(
'.post-list',
posts.map(function (post) {
return m(Post, { key: post.id, post: post });
})
);
// In the `Post` component
m(
'.comment-list',
comments.map(function (comment) {
return m(Comment, { key: comment.id, comment: comment });
})
);
Notez que pour les commentaires, bien que cela fonctionnerait techniquement sans clés dans ce cas, cela se briserait de même si vous deviez ajouter quoi que ce soit comme des commentaires imbriqués ou la possibilité de les modifier, et vous devriez leur ajouter des clés.
Garder les collections d'objets animés sans accrocs
Dans certains cas, vous souhaiterez peut-être animer des listes, des boîtes et des éléments similaires. Commençons par ce code simple :
var colors = ['red', 'yellow', 'blue', 'gray'];
var counter = 0;
function getColor() {
var color = colors[counter];
counter = (counter + 1) % colors.length;
return color;
}
function Boxes() {
var boxes = [];
function add() {
boxes.push({ color: getColor() });
}
function remove(box) {
var index = boxes.indexOf(box);
boxes.splice(index, 1);
}
return {
view: function () {
return [
m('button', { onclick: add }, 'Add box, click box to remove'),
m(
'.container',
boxes.map(function (box, i) {
return m(
'.box',
{
'data-color': box.color,
onclick: function () {
remove(box);
},
},
m('.stretch')
);
})
),
];
},
};
}
Ce code semble inoffensif, mais essayez un exemple en direct. Dans cet exemple, cliquez pour créer quelques boîtes, sélectionnez une boîte et observez sa taille. Nous voulons que la taille et la rotation soient liées à la boîte (indiquée par la couleur) et non à la position dans la grille. Vous remarquerez qu'au lieu de cela, la taille varie brusquement, mais reste constante avec l'emplacement. Cela signifie que nous devons leur attribuer des keys (clés).
Dans ce cas, il est assez facile de leur attribuer des clés uniques : il suffit de créer un compteur que vous incrémentez à chaque ajout.
var colors = ['red', 'yellow', 'blue', 'gray'];
var counter = 0;
function getColor() {
var color = colors[counter];
counter = (counter + 1) % colors.length;
return color;
}
function Boxes() {
var boxes = [];
var nextKey = 0;
function add() {
boxes.push({ color: getColor() });
var key = nextKey;
nextKey++;
boxes.push({ key: key, color: getColor() });
}
function remove(box) {
var index = boxes.indexOf(box);
boxes.splice(index, 1);
}
return {
view: function () {
return [
m('button', { onclick: add }, 'Add box, click box to remove'),
m(
'.container',
boxes.map(function (box, i) {
return m(
'.box',
{
key: box.key,
'data-color': box.color,
onclick: function () {
remove(box);
},
},
m('.stretch')
);
})
),
];
},
};
}
Réinitialiser les vues avec des fragments à clé unique
Lorsque vous manipulez des entités avec état dans des modèles, il est souvent utile de rendre les vues de modèles avec des keys (clés). Supposons que vous ayez cette disposition :
function Layout() {
// ...
}
function Person() {
// ...
}
m.route(rootElem, '/', {
'/': Home,
'/person/:id': {
render: function () {
return m(Layout, m(Person, { id: m.route.param('id') }));
},
},
// ...
});
Il est probable que votre composant Person
ressemble à ceci :
function Person(vnode) {
var personId = vnode.attrs.id;
var state = 'pending';
var person, error;
m.request('/api/person/:id', { params: { id: personId } }).then(
function (p) {
person = p;
state = 'ready';
},
function (e) {
error = e;
state = 'error';
}
);
return {
view: function () {
if (state === 'pending') return m(LoadingIcon);
if (state === 'error') {
return error.code === 404
? m('.person-missing', 'Person not found.')
: m('.person-error', 'An error occurred. Please try again later');
}
return m(
'.person',
m(
m.route.Link,
{
class: 'person-edit',
href: '/person/:id/edit',
params: { id: personId },
},
'Edit'
),
m('.person-name', 'Name: ', person.name)
// ...
);
},
};
}
Supposons que vous ayez ajouté un moyen de créer un lien vers d'autres personnes à partir de ce composant, par exemple en ajoutant un champ "manager" (gestionnaire).
function Person(vnode) {
// ...
return {
view: function () {
// ...
return m(
'.person',
m(
m.route.Link,
{
class: 'person-edit',
href: '/person/:id/edit',
params: { id: personId },
},
'Edit'
),
m('.person-name', person.name),
// ...
m(
'.manager',
'Manager: ',
m(
m.route.Link,
{
href: '/person/:id',
params: { id: person.manager.id },
},
person.manager.name
)
)
// ...
);
},
};
}
Si l'ID de la personne est 1
et que l'ID du gestionnaire est 2
, vous passeriez de /person/1
à /person/2
, en restant sur la même route. Mais puisque vous avez utilisé la méthode de résolution de route render
, l'arbre a été conservé et vous venez de passer de m(Layout, m(Person, {id: "1"}))
à m(Layout, m(Person, {id: "2"}))
. Dans ce cas, le composant Person
n'a pas changé, et donc il ne se réinitialise pas. Mais dans notre cas, cela pose problème, car cela signifie que les données du nouvel utilisateur ne sont pas récupérées. C'est là que les clés entrent en jeu. Nous pourrions modifier le résolveur de route comme ceci pour corriger cela :
m.route(rootElem, '/', {
'/': Home,
'/person/:id': {
render: function () {
return m(
Layout,
// Entourez-le d'un tableau au cas où nous ajouterions d'autres éléments plus tard.
// Rappelez-vous : les fragments doivent contenir soit uniquement des enfants avec des clés, soit aucun enfant avec des clés.
[m(Person, { id: m.route.param('id'), key: m.route.param('id') })]
);
},
},
// ...
});
Erreurs fréquentes
Il existe plusieurs erreurs fréquentes liées à l'utilisation des clés. Voici quelques exemples pour vous aider à comprendre pourquoi elles ne fonctionnent pas.
Encapsuler des éléments avec des clés
Ces deux extraits de code ne se comportent pas de la même manière :
users.map(function (user) {
return m('.wrapper', [m(User, { user: user, key: user.id })]);
});
users.map(function (user) {
return m('.wrapper', { key: user.id }, [m(User, { user: user })]);
});
Dans le premier cas, la clé est associée au composant User
, mais le fragment externe créé par users.map(...)
n'a pas de clé. Encapsuler un élément avec une clé de cette manière est inefficace et peut entraîner des comportements imprévisibles, allant de requêtes supplémentaires à chaque modification de la liste à la perte de l'état des champs de formulaire internes. Le comportement résultant serait similaire à l'exemple erroné de la liste d'articles, mais sans le problème de corruption d'état.
Dans le second cas, la clé est associée à l'élément .wrapper
, garantissant que le fragment externe a une clé. C'est probablement le comportement souhaité, et la suppression d'un utilisateur n'affectera pas l'état des autres instances d'utilisateur.
Placer des clés à l'intérieur du composant
Supposons que, dans l'exemple de personne, vous ayez fait ceci à la place :
// À ÉVITER
function Person(vnode) {
var personId = vnode.attrs.id;
// ...
return {
view: function () {
return m.fragment(
{ key: personId }
// ce que vous aviez précédemment dans la vue
);
},
};
}
Cela ne fonctionnera pas, car la clé ne s'applique pas au composant dans son ensemble. Elle s'applique uniquement à la vue, et vous ne récupérez donc pas les données comme prévu.
Il est préférable d'utiliser la solution présentée dans l'exemple, en plaçant la clé dans le vnode utilisant le composant plutôt qu'à l'intérieur du composant lui-même.
// PRÉFÉRER
return [m(Person, { id: m.route.param('id'), key: m.route.param('id') })];
Utiliser des clés inutilement
Une idée fausse courante est de penser que les clés sont des identifiants en soi. Mithril.js exige que tous les enfants d'un fragment aient soit tous des clés, soit aucun, et générera une erreur si cette règle n'est pas respectée. Supposons que vous ayez la structure suivante :
m('.page', m('.header', { key: 'header' }), m('.body'), m('.footer'));
Cela générera une erreur, car .header
a une clé, tandis que .body
et .footer
n'en ont pas. Il est important de comprendre que vous n'avez pas besoin de clés dans ce cas. Si vous vous retrouvez à utiliser des clés pour des éléments comme celui-ci, la solution n'est pas d'ajouter des clés, mais de les supprimer. N'ajoutez des clés que si vous en avez vraiment besoin. Oui, les nœuds DOM sous-jacents ont des identités, mais Mithril.js n'a pas besoin de suivre ces identités pour les mettre à jour correctement. Il le fait rarement. Les clés sont nécessaires uniquement avec des listes où chaque entrée a un état associé que Mithril.js ne suit pas lui-même, que ce soit dans un modèle, un composant ou le DOM.
Enfin, évitez les clés statiques. Elles sont toujours inutiles. Si vous ne calculez pas votre attribut key
, vous faites probablement quelque chose de mal.
Notez que si vous avez vraiment besoin d'un seul élément avec une clé de manière isolée, utilisez un fragment avec une clé à enfant unique. Il s'agit simplement d'un tableau avec un seul enfant qui est un élément avec une clé, comme [m("div", {key: foo})]
.
Combiner différents types de clés
Les clés sont interprétées comme des noms de propriétés d'objet. Cela signifie que 1
et "1"
sont traités de la même manière. Pour éviter les problèmes, évitez de mélanger les types de clés autant que possible. Si vous le faites, vous risquez de vous retrouver avec des clés en double et un comportement inattendu.
// À ÉVITER
var things = [
{ id: '1', name: 'Book' },
{ id: 1, name: 'Cup' },
];
Si vous devez absolument le faire et que vous n'avez aucun contrôle sur cela, utilisez un préfixe indiquant son type pour qu'ils restent distincts.
things.map(function (thing) {
return m(
'.thing',
{ key: typeof thing.id + ':' + thing.id }
// ...
);
});
Masquer des éléments avec des clés en utilisant des "trous"
Les valeurs telles que null
, undefined
et les booléens sont considérées comme des vnodes sans clé, donc un code comme celui-ci ne fonctionnera pas :
// À ÉVITER
things.map(function (thing) {
return shouldShowThing(thing)
? m(Thing, { key: thing.id, thing: thing })
: null;
});
Au lieu de cela, filtrez la liste avant de la renvoyer, et Mithril.js fera ce qu'il faut. La plupart du temps, Array.prototype.filter
est précisément ce dont vous avez besoin et vous devriez certainement l'essayer.
// PRÉFÉRER
things
.filter(function (thing) {
return shouldShowThing(thing);
})
.map(function (thing) {
return m(Thing, { key: thing.id, thing: thing });
});
Clés en double
Les clés pour les éléments d'un fragment doivent être uniques. Sinon, il est impossible de déterminer quelle clé doit être utilisée. Vous risquez également de rencontrer des problèmes avec des éléments qui ne se déplacent pas comme prévu.
// À ÉVITER
var things = [
{ id: '1', name: 'Book' },
{ id: '1', name: 'Cup' },
];
Mithril.js utilise un objet vide pour mapper les clés aux indices afin de savoir comment mettre à jour correctement les fragments avec des clés. Lorsque vous avez une clé en double, il n'est plus clair où cet élément s'est déplacé, et Mithril.js se comportera de manière inattendue lors de la mise à jour, surtout si la liste a changé. Des clés distinctes sont nécessaires pour que Mithril.js puisse correctement connecter les anciens aux nouveaux nœuds. Vous devez donc choisir quelque chose d'unique localement à utiliser comme clé.
Utiliser des objets comme clés
Les clés pour les éléments d'un fragment sont traitées comme des clés de propriété. Utiliser des objets comme clés ne fonctionnera pas comme prévu.
// À ÉVITER
things.map(function (thing) {
return m(Thing, { key: thing, thing: thing });
});
Si l'objet a une méthode toString
, celle-ci sera appelée, et vous serez tributaire de ce qu'elle renvoie, sans même vous rendre compte que cette méthode est appelée. Si ce n'est pas le cas, tous vos objets seront transformés en chaîne en "[object Object]"
et vous rencontrerez donc un problème de clé en double.