Claves
¿Qué son las claves?
Las claves representan identidades que Mithril.js rastrea. Puedes agregarlas a vnodes de elementos, componentes y fragmentos a través del atributo key
. Su uso se ve así:
m('.user', { key: user.id }, [
/* ... */
]);
Son útiles en los siguientes casos:
- Cuando renderizas datos de un modelo u otros datos con estado, necesitas claves para mantener el estado local asociado al subárbol correcto.
- Cuando animas independientemente múltiples nodos adyacentes usando CSS y existe la posibilidad de eliminar alguno de ellos individualmente, necesitas claves para asegurar que las animaciones permanezcan asociadas a los elementos correctos y no se trasladen inesperadamente a otros nodos.
- Cuando necesitas reinicializar un subárbol bajo demanda, agrega una clave, cámbiala y fuerza un redibujo cada vez que desees reinicializarlo.
Restricciones de las claves
Importante: En fragmentos, sus hijos deben ser exclusivamente vnodes con atributos de clave (fragmento con clave) o exclusivamente vnodes sin atributos de clave (fragmento sin clave). Los atributos de clave solo pueden existir en vnodes que admiten atributos, es decir, vnodes de elementos, componentes y fragmentos. Otros vnodes, como null
, undefined
y strings, no pueden tener atributos de ningún tipo, por lo que no pueden tener atributos de clave y, por lo tanto, no se pueden usar en fragmentos con clave.
Esto significa que expresiones como [m(".foo", {key: 1}), null]
y ["foo", m(".bar", {key: 2})]
no funcionarán, mientras que [m(".foo", {key: 1}), m(".bar", {key: 2})]
y [m(".foo"), null]
sí lo harán. Si olvidas esto, obtendrás un error informativo que lo explica.
Vinculación de datos del modelo en listas de vistas
Al renderizar listas, especialmente listas editables, a menudo trabajas con elementos como TODOs editables y similares. Estos elementos tienen estado e identidades, y debes proporcionar a Mithril.js la información necesaria para rastrearlos.
Supongamos que tenemos una lista de publicaciones en redes sociales, donde puedes comentar en las publicaciones y ocultarlas por diversos motivos, como reportarlas.
// `User` y `ComposeWindow` omitidos por brevedad
function CommentCompose() {
return {
view: function (vnode) {
var post = vnode.attrs.post;
return m(ComposeWindow, {
placeholder: 'Escribe tu comentario...',
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);
},
},
"No me gusta esto"
)
);
},
};
}
function PostCompose() {
return {
view: function (vnode) {
var comment = vnode.attrs.comment;
return m(ComposeWindow, {
placeholder: 'Escribe tu publicación...',
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,
' comentario',
post.commentCount === 1 ? '' : 's'
),
m(
'a.post-hide',
{
onclick: function () {
Model.hidePost(post).then(m.redraw);
},
},
"No me gusta esto"
)
),
showComments
? m(
'.post-comments',
comments == null
? m('.comment-list-loading', 'Cargando...')
: [
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', 'Cargando...')
: m(
'.post-view',
m(PostCompose),
m(
'.post-list',
posts.map(function (post) {
return m(Post, { post: post });
})
)
)
);
},
};
}
Este código encapsula mucha funcionalidad, pero quiero centrarme en dos partes:
// En el componente `Feed`
m(
'.post-list',
posts.map(function (post) {
return m(Post, { post: post });
})
);
// En el componente `Post`
m(
'.comment-list',
comments.map(function (comment) {
return m(Comment, { comment: comment });
})
);
Cada uno de estos fragmentos se refiere a un subárbol con estado asociado del que Mithril.js no tiene conocimiento. (Mithril.js solo conoce los vnodes). Cuando los dejas sin clave, pueden ocurrir comportamientos extraños e inesperados. Por ejemplo, haz clic en "N comentarios" para mostrar los comentarios. Luego, escribe algo en el cuadro de comentarios en la parte inferior y haz clic en "No me gusta esto" en una publicación anterior. Aquí hay una demostración en vivo para que la pruebes, completa con un modelo simulado. (Nota: si estás en Edge o IE, puedes encontrar problemas debido a la longitud del hash del enlace).
En lugar de comportarse como esperarías, se comporta de manera errónea: cierra la lista de comentarios que tenías abierta y la publicación siguiente a aquella cuyos comentarios estaban abiertos ahora muestra persistentemente "Cargando..." aunque cree que ya ha cargado los comentarios. Esto ocurre porque los comentarios se cargan de forma diferida y el código asume que siempre se pasa el mismo comentario (lo cual parece lógico), pero en este caso no es así. Esto se debe a cómo Mithril.js actualiza los fragmentos sin clave: los actualiza uno por uno iterativamente de una manera muy simple. Entonces, en este caso, la diferencia sería similar a:
- Antes:
A, B, C, D, E
- Parcheado:
A, B, C -> D, D -> E, E -> (eliminado)
Y dado que el componente sigue siendo el mismo (siempre es Comment
), solo cambian los atributos y no se reemplaza.
Para solucionar este error, basta con agregar una clave para que Mithril.js gestione correctamente el estado. Aquí hay un ejemplo en vivo y funcional de todo arreglado.
// En el componente `Feed`
m(
'.post-list',
posts.map(function (post) {
return m(Post, { key: post.id, post: post });
})
);
// En el componente `Post`
m(
'.comment-list',
comments.map(function (comment) {
return m(Comment, { key: comment.id, comment: comment });
})
);
Ten en cuenta que para los comentarios, aunque técnicamente funcionaría sin claves en este caso, se rompería de manera similar si agregaras algo como comentarios anidados o la capacidad de editarlos, y tendrías que agregarles claves.
Mantener colecciones de objetos animados sin problemas
En ocasiones, es posible que quieras animar listas, contenedores y elementos similares. Empecemos con este código sencillo:
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 }, 'Añadir caja, click en la caja para eliminar'),
m(
'.container',
boxes.map(function (box, i) {
return m(
'.box',
{
'data-color': box.color,
onclick: function () {
remove(box);
},
},
m('.stretch')
);
})
),
];
},
};
}
Parece bastante simple, pero prueba este ejemplo en vivo. En ese ejemplo, crea varias cajas haciendo clic, elige una y observa su tamaño. Queremos que el tamaño y la rotación estén asociados a la caja (identificada por el color) y no a su posición en la cuadrícula. Notarás que el tamaño cambia bruscamente, pero se mantiene constante con la ubicación. Esto significa que necesitamos asignarles keys.
En este caso, asignar keys únicas es bastante fácil: simplemente crea un contador que incrementes cada vez que lo uses.
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 }, 'Añadir caja, click en la caja para eliminar'),
m(
'.container',
boxes.map(function (box, i) {
return m(
'.box',
{
key: box.key,
'data-color': box.color,
onclick: function () {
remove(box);
},
},
m('.stretch')
);
})
),
];
},
};
}
Reinicialización de vistas con fragmentos con key de un solo hijo
Cuando trabajas con entidades con estado en modelos y similares, a menudo es útil renderizar vistas de modelo con keys. Supongamos que tienes este diseño:
function Layout() {
// ...
}
function Person() {
// ...
}
m.route(rootElem, '/', {
'/': Home,
'/person/:id': {
render: function () {
return m(Layout, m(Person, { id: m.route.param('id') }));
},
},
// ...
});
Probablemente tu componente Person
se vea algo así:
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', 'Persona no encontrada.')
: m('.person-error', 'Ocurrió un error. Por favor, inténtalo de nuevo más tarde');
}
return m(
'.person',
m(
m.route.Link,
{
class: 'person-edit',
href: '/person/:id/edit',
params: { id: personId },
},
'Editar'
),
m('.person-name', 'Nombre: ', person.name)
// ...
);
},
};
}
Supongamos que añadiste una forma de enlazar a otras personas desde este componente, como por ejemplo, agregar un campo "manager" (gerente).
function Person(vnode) {
// ...
return {
view: function () {
// ...
return m(
'.person',
m(
m.route.Link,
{
class: 'person-edit',
href: '/person/:id/edit',
params: { id: personId },
},
'Editar'
),
m('.person-name', person.name),
// ...
m(
'.manager',
'Gerente: ',
m(
m.route.Link,
{
href: '/person/:id',
params: { id: person.manager.id },
},
person.manager.name
)
)
// ...
);
},
};
}
Suponiendo que el ID de la persona fuera 1
y el ID del gerente fuera 2
, cambiarías de /person/1
a /person/2
, manteniéndote en la misma ruta. Pero como usaste el método render
del resolvedor de rutas, el árbol se mantuvo y simplemente cambiaste de m(Layout, m(Person, {id: "1"}))
a m(Layout, m(Person, {id: "2"}))
. En este caso, el componente Person
no cambió, por lo que no se reinicializa. Pero para nuestro caso, esto es problemático, porque significa que no se está recuperando la información del nuevo usuario. Aquí es donde las keys resultan útiles. Podríamos modificar el resolvedor de rutas de la siguiente manera para solucionar este problema:
m.route(rootElem, '/', {
'/': Home,
'/person/:id': {
render: function () {
return m(
Layout,
// Inclúyelo en un array en caso de que agreguemos otros elementos en el futuro.
// Recuerda: los fragmentos deben contener exclusivamente elementos hijos con *key*, o ningún elemento hijo con *key*.
[m(Person, { id: m.route.param('id'), key: m.route.param('id') })]
);
},
},
// ...
});
Errores comunes
Es común encontrarse con algunos errores al usar claves. Aquí hay algunos de ellos, para ayudarte a entender por qué no funcionan.
Envolver elementos con clave
Estos dos fragmentos no funcionan de la misma manera:
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 })]);
});
El primero asocia la clave al componente User
, pero el fragmento externo creado por users.map(...)
no tiene clave. Envolver un elemento con clave de esta manera no funciona, y el resultado podría ser cualquier cosa, desde solicitudes adicionales cada vez que se modifica la lista hasta que los campos de formulario internos pierdan su estado. El comportamiento resultante sería similar al ejemplo roto de la lista de publicaciones, pero sin el problema de la corrupción del estado.
El segundo asocia la clave al elemento .wrapper
, asegurando que el fragmento externo sí tenga clave. Esto logra el resultado deseado, y eliminar un usuario no afectará el estado de otras instancias de usuario.
Poner claves dentro del componente
Supongamos que, en el ejemplo de persona, hicieras esto en su lugar:
// EVITAR
function Person(vnode) {
var personId = vnode.attrs.id;
// ...
return {
view: function () {
return m.fragment(
{ key: personId }
// lo que tenías antes en la vista
);
},
};
}
Esto no funcionará, porque la clave no se aplica al componente en su totalidad. Simplemente se aplica a la vista, por lo que los datos no se recuperan como se espera.
Es preferible la solución utilizada allí, colocando la clave en el vnode al usar el componente en lugar de dentro del componente en sí.
// PREFERIR
return [m(Person, { id: m.route.param('id'), key: m.route.param('id') })];
Asignar claves a elementos innecesariamente
Es un error común pensar que las claves son identidades por sí mismas. Mithril.js requiere que todos los fragmentos tengan hijos con claves o sin ellas, y generará un error si lo olvidas. Supongamos que tienes este diseño:
m('.page', m('.header', { key: 'header' }), m('.body'), m('.footer'));
Obviamente, esto generará un error, ya que .header
tiene una clave y .body
y .footer
no la tienen. Sin embargo, no se necesitan claves para esto. Si se usan claves en situaciones como esta, la solución es eliminarlas, no agregarlas. Solo agrégalas si realmente, realmente las necesitas. Sí, los nodos DOM subyacentes tienen identidades, pero Mithril.js no necesita rastrear esas identidades para actualizarlos correctamente. Prácticamente nunca lo hace. Solo necesitas claves con listas donde cada entrada tiene algún tipo de estado asociado que Mithril.js no rastrea por sí mismo, ya sea en un modelo, en un componente o en el propio DOM.
Un consejo final: evita las claves estáticas. Siempre son innecesarias. Si no estás calculando tu atributo key
, probablemente estés haciendo algo mal.
Ten en cuenta que si realmente necesitas un solo elemento con clave de forma aislada, usa un fragmento con clave de un solo hijo. Es simplemente un array con un solo hijo que es un elemento con clave, como [m("div", {key: foo})]
.
Mezclar tipos de clave
Las claves se interpretan como nombres de propiedades de objeto. Esto significa que 1
y "1"
se tratan de forma idéntica. Para evitar problemas, no mezcles tipos de clave si es posible. Si lo haces, podrías terminar con claves duplicadas y un comportamiento inesperado.
// EVITAR
var things = [
{ id: '1', name: 'Book' },
{ id: 1, name: 'Cup' },
];
Si es imprescindible y no tienes control sobre ello, usa un prefijo para distinguir los tipos.
things.map(function (thing) {
return m(
'.thing',
{ key: typeof thing.id + ':' + thing.id }
// ...
);
});
Ocultar elementos con clave con huecos
Valores como null
, undefined
y los booleanos se consideran vnodes sin clave, por lo tanto, este código no funcionará:
// EVITAR
things.map(function (thing) {
return shouldShowThing(thing)
? m(Thing, { key: thing.id, thing: thing })
: null;
});
En su lugar, filtra la lista antes de devolverla, y Mithril.js hará lo correcto. En la mayoría de los casos, Array.prototype.filter
es la solución adecuada.
// PREFERIR
things
.filter(function (thing) {
return shouldShowThing(thing);
})
.map(function (thing) {
return m(Thing, { key: thing.id, thing: thing });
});
Claves duplicadas
Las claves para los elementos de fragmento deben ser únicas, de lo contrario, no quedará claro a qué elemento corresponde cada clave. También puedes tener problemas con elementos que no se mueven como se supone que deben hacerlo.
// EVITAR
var things = [
{ id: '1', name: 'Book' },
{ id: '1', name: 'Cup' },
];
Mithril.js usa un objeto vacío para asociar claves con índices para saber cómo parchear correctamente los fragmentos con clave. Cuando tienes una clave duplicada, ya no está claro a dónde se movió ese elemento, por lo que Mithril.js se comportará de forma inesperada en la actualización, especialmente si la lista ha cambiado. Se requieren claves distintas para que Mithril.js conecte correctamente los nodos antiguos con los nuevos, por lo que debes elegir algo localmente único para usar como clave.
Usar objetos para claves
Las claves para los elementos de fragmento se tratan como claves de propiedad. Este tipo de construcciones no funcionarán como se espera.
// EVITAR
things.map(function (thing) {
return m(Thing, { key: thing, thing: thing });
});
Si el objeto tiene un método toString
, se llamaría a ese método, y dependerías de lo que devuelva, posiblemente sin darte cuenta de que incluso se está llamando a ese método. Si no lo tiene, todos tus objetos se convertirán en la cadena "[object Object]"
y, por lo tanto, se producirá un problema de clave duplicada.