Keys
O que são keys?
As keys representam identidades rastreadas. Você pode adicioná-las aos vnodes de elementos, componentes e fragmentos através do atributo mágico key
. A sintaxe de uso é a seguinte:
m('.user', { key: user.id }, [
/* ... */
]);
Elas são úteis nos seguintes cenários:
- Ao renderizar dados de modelo ou outros dados com estado, você precisa de keys para manter o estado local vinculado à subárvore correta.
- Ao animar independentemente múltiplos nós adjacentes usando CSS e você pode remover qualquer um deles individualmente, você precisa de keys para garantir que as animações permaneçam com os elementos corretos e não sejam aplicadas a outros nós inesperadamente.
- Ao precisar reinicializar uma subárvore sob demanda, adicione uma key e altere seu valor para forçar a redesenhar sempre que quiser reinicializá-la.
Restrições de key
Importante: Para todos os fragmentos, seus filhos devem conter exclusivamente vnodes com atributos key (fragmento com chave) ou exclusivamente vnodes sem atributos key (fragmento sem chave). Atributos key só podem existir em vnodes que suportam atributos, como vnodes de elementos, componentes e fragmentos. Outros vnodes, como null
, undefined
e strings, não podem ter atributos de qualquer tipo e, portanto, não podem ter atributos key, o que impede seu uso em fragmentos com chave.
Isso significa que [m(".foo", {key: 1}), null]
e ["foo", m(".bar", {key: 2})]
não funcionarão, mas [m(".foo", {key: 1}), m(".bar", {key: 2})]
e [m(".foo"), null]
funcionarão. Se você esquecer disso, receberá uma mensagem de erro útil explicando o problema.
Vinculando dados de modelo em listas de views (visualizações)
Ao renderizar listas, especialmente listas editáveis, você frequentemente lida com elementos como TODOs editáveis. Estes elementos têm estado e identidades, e você precisa fornecer ao Mithril.js as informações necessárias para rastreá-los.
Suponha que temos uma listagem simples de posts de mídia social, onde você pode comentar e esconder posts.
// `User` e `ComposeWindow` omitidos para brevidade
function CommentCompose() {
return {
view: function (vnode) {
var post = vnode.attrs.post;
return m(ComposeWindow, {
placeholder: 'Write your comment...',
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);
},
},
"I don't like this"
)
);
},
};
}
function PostCompose() {
return {
view: function (vnode) {
var comment = vnode.attrs.comment;
return m(ComposeWindow, {
placeholder: 'Write your post...',
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,
' comment',
post.commentCount === 1 ? '' : 's'
),
m(
'a.post-hide',
{
onclick: function () {
Model.hidePost(post).then(m.redraw);
},
},
"I don't like this"
)
),
showComments
? m(
'.post-comments',
comments == null
? m('.comment-list-loading', 'Loading...')
: [
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', 'Loading...')
: m(
'.post-view',
m(PostCompose),
m(
'.post-list',
posts.map(function (post) {
return m(Post, { post: post });
})
)
)
);
},
};
}
Este código encapsula muita funcionalidade, mas vamos focar em dois trechos:
// No componente `Feed`
m(
'.post-list',
posts.map(function (post) {
return m(Post, { post: post });
})
);
// No componente `Post`
m(
'.comment-list',
comments.map(function (comment) {
return m(Comment, { comment: comment });
})
);
Cada um desses trechos se refere a uma subárvore com estado associado que o Mithril.js não conhece (Mithril.js só conhece vnodes). Quando você omite as keys, o comportamento pode se tornar estranho e inesperado. Neste caso, tente clicar em "N comments" para mostrar os comentários, digitar algo na caixa de composição de comentários na parte inferior e, em seguida, clicar em "I don't like this" em um post acima. Aqui está uma demonstração ao vivo para você experimentar, completa com um modelo mock. (Note: if you're on Edge or IE, you may run into issues due to the link's hash length.)
Em vez de funcionar como esperado, o comportamento se torna confuso e incorreto: a lista de comentários que você abriu é fechada, e o post seguinte ao que você abriu os comentários agora mostra persistentemente "Loading...", mesmo que o sistema acredite que já carregou os comentários. Isso ocorre porque os comentários são carregados de forma lazy e o sistema assume que o mesmo comentário é passado a cada vez (o que parece razoável aqui), mas neste caso, não é. Isso acontece devido à forma como o Mithril.js aplica o patch de fragmentos sem key: ele aplica o patch iterativamente, um por um, de forma muito simples. Então, neste caso, a diferença pode ser algo como:
- Before:
A, B, C, D, E
- Patched:
A, B, C -> D, D -> E, E -> (removed)
E como o componente permanece o mesmo (é sempre Comment
), apenas os atributos mudam e ele não é substituído.
Para corrigir esse bug, basta adicionar uma key para que o Mithril.js saiba mover o estado, se necessário. Aqui está um exemplo ao vivo e funcionando de tudo corrigido.
// No componente `Feed`
m(
'.post-list',
posts.map(function (post) {
return m(Post, { key: post.id, post: post });
})
);
// No componente `Post`
m(
'.comment-list',
comments.map(function (comment) {
return m(Comment, { key: comment.id, comment: comment });
})
);
Observe que, para os comentários, embora tecnicamente funcionasse sem chaves neste caso, da mesma forma quebraria se você adicionasse algo como comentários aninhados ou a capacidade de editá-los, e você teria que adicionar chaves a eles.
Mantendo coleções de objetos animados sem problemas
Em algumas situações, você pode querer animar listas, elementos em caixas e outros componentes. Vamos começar com este código simples:
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 }, 'Adicionar caixa, clique na caixa para remover'),
m(
'.container',
boxes.map(function (box, i) {
return m(
'.box',
{
'data-color': box.color,
onclick: function () {
remove(box);
},
},
m('.stretch')
);
})
),
];
},
};
}
À primeira vista, parece correto, mas experimente este exemplo ao vivo. Nesse exemplo, adicione algumas caixas clicando no botão, escolha uma caixa e observe seu tamanho. O objetivo é que o tamanho e a rotação estejam vinculados à caixa (identificada pela cor) e não à sua posição na grade. Você notará que o tamanho muda repentinamente, mas permanece constante em relação à localização. Isso indica que precisamos atribuir chaves (keys) a esses elementos.
Nesse caso, atribuir chaves exclusivas é bem simples: basta criar um contador que é incrementado a cada leitura.
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 }, 'Adicionar caixa, clique na caixa para remover'),
m(
'.container',
boxes.map(function (box, i) {
return m(
'.box',
{
key: box.key,
'data-color': box.color,
onclick: function () {
remove(box);
},
},
m('.stretch')
);
})
),
];
},
};
}
Aqui está um exemplo corrigido para você testar e ver como funciona de forma diferente.
Reinicializando visualizações com fragmentos chaveados de filho único
Ao lidar com entidades com estado em templates, geralmente é útil renderizar visualizações de template com chaves (keys). Suponha que você tenha o seguinte layout:
function Layout() {
// ...
}
function Person() {
// ...
}
m.route(rootElem, '/', {
'/': Home,
'/person/:id': {
render: function () {
return m(Layout, m(Person, { id: m.route.param('id') }));
},
},
// ...
});
É provável que seu componente Person
se pareça com isto:
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', 'Pessoa não encontrada')
: m('.person-error', 'Ocorreu um erro. Por favor, tente novamente mais tarde');
}
return m(
'.person',
m(
m.route.Link,
{
class: 'person-edit',
href: '/person/:id/edit',
params: { id: personId },
},
'Editar'
),
m('.person-name', 'Nome: ', person.name)
// ...
);
},
};
}
Digamos que você adicionou uma forma de vincular a outras pessoas a partir deste componente, como, por exemplo, adicionar um campo "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
)
)
// ...
);
},
};
}
Assumindo que o ID da pessoa era 1
e o ID do gerente era 2
, você mudaria de /person/1
para /person/2
, permanecendo na mesma rota. No entanto, como você usou o método render
do resolvedor de rotas, a árvore foi retida e você apenas mudou de m(Layout, m(Person, {id: "1"}))
para m(Layout, m(Person, {id: "2"}))
. Nesse caso, o componente Person
não foi alterado e, portanto, não é reinicializado. Mas, para o nosso caso, isso é problemático, porque significa que o novo usuário não está sendo buscado. É aqui que as chaves (keys) se tornam úteis. Poderíamos modificar o resolvedor de rotas para o seguinte para corrigir isso:
m.route(rootElem, '/', {
'/': Home,
'/person/:id': {
render: function () {
return m(
Layout,
// Envolva-o em um array caso adicionemos outros elementos mais tarde.
// Lembre-se: fragmentos devem conter apenas filhos com chave ou nenhum filho sem chave.
[m(Person, { id: m.route.param('id'), key: m.route.param('id') })]
);
},
},
// ...
});
Armadilhas Comuns
Existem várias armadilhas comuns que as pessoas encontram ao usar chaves. Aqui estão algumas delas, para ajudá-lo a entender por que certas abordagens não funcionam.
Envolvendo Elementos com Chaves
Estes dois trechos de código não funcionam da mesma maneira:
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 })]);
});
O primeiro associa a chave ao componente User
, mas o fragmento externo criado por users.map(...)
não possui nenhuma chave. Envolver um elemento com chave dessa forma não funciona, e o resultado pode variar desde requisições extras a cada alteração da lista até a perda do estado de campos de formulário internos. O comportamento resultante seria semelhante ao exemplo quebrado da lista de posts, mas sem o problema de corrupção de estado.
O segundo associa a chave ao elemento .wrapper
, garantindo que o fragmento externo tenha uma chave. Isso faz o que você provavelmente queria desde o início, e remover um usuário não causará problemas com o estado de outras instâncias de usuário.
Colocando Chaves Dentro do Componente
Suponha que, no exemplo de pessoa, você fizesse isso em vez disso:
// NÃO RECOMENDADO
function Person(vnode) {
var personId = vnode.attrs.id;
// ...
return {
view: function () {
return m.fragment(
{ key: personId }
// o que você tinha anteriormente na view
);
},
};
}
Isso não funcionará, porque a chave não se aplica ao componente como um todo. Ela se aplica apenas à view e, portanto, você não estará buscando os dados novamente como esperava.
A solução RECOMENDADA é colocar a chave no vnode usando o componente, em vez de dentro do próprio componente.
// RECOMENDADO
return [m(Person, { id: m.route.param('id'), key: m.route.param('id') })];
Usando Chaves Desnecessariamente
É um equívoco comum que as chaves são identidades em si mesmas. Mithril.js exige que, para todos os fragmentos, seus filhos devam ter todas as chaves ou não ter nenhuma chave, e gerará um erro se você esquecer disso. Suponha que você tenha este layout:
m('.page', m('.header', { key: 'header' }), m('.body'), m('.footer'));
Isso obviamente gerará um erro, pois .header
tem uma chave e .body
e .footer
não têm chaves. Mas aqui está a questão: você não precisa de chaves para isso. Se você se pegar usando chaves para coisas assim, a solução não é adicionar chaves, mas removê-las. Adicione-as apenas se você realmente, realmente precisar delas. Sim, os nós DOM subjacentes têm identidades, mas Mithril.js não precisa rastrear essas identidades para corrigi-los corretamente. Praticamente nunca precisa. Apenas com listas onde cada entrada tem algum tipo de estado associado que o próprio Mithril.js não rastreia, seja em um modelo, em um componente ou no próprio DOM, você precisa de chaves.
Uma última coisa: evite chaves estáticas. Elas são sempre desnecessárias. Se você não está computando seu atributo key
, provavelmente está fazendo algo errado.
Observe que, se você realmente precisar de um único elemento com chave isolada, use um fragmento com chave de filho único. É apenas um array com um único filho que é um elemento com chave, como [m("div", {key: foo})]
.
Misturando Tipos de Chave
As chaves são lidas como nomes de propriedades de objeto. Isso significa que 1
e "1"
são tratados de forma idêntica. Se você quiser evitar dores de cabeça, não misture tipos de chave se puder evitar. Se você fizer isso, poderá acabar com chaves duplicadas e comportamento inesperado.
// NÃO RECOMENDADO
var things = [
{ id: '1', name: 'Book' },
{ id: 1, name: 'Cup' },
];
Se você absolutamente precisar e não tiver controle sobre isso, use um prefixo denotando seu tipo para que permaneçam distintos.
things.map(function (thing) {
return m(
'.thing',
{ key: typeof thing.id + ':' + thing.id }
// ...
);
});
Ocultando Elementos com Chave com "Buracos"
Valores vazios como null
, undefined
e booleanos são considerados vnodes sem chave, então um código como este não funcionará:
// NÃO RECOMENDADO
things.map(function (thing) {
return shouldShowThing(thing)
? m(Thing, { key: thing.id, thing: thing })
: null;
});
Em vez disso, filtre a lista antes de retorná-la, e Mithril.js se comportará corretamente. Na maioria das vezes, Array.prototype.filter
é precisamente o que você precisa e você definitivamente deveria experimentá-lo.
// RECOMENDADO
things
.filter(function (thing) {
return shouldShowThing(thing);
})
.map(function (thing) {
return m(Thing, { key: thing.id, thing: thing });
});
Chaves Duplicadas
As chaves para itens de fragmento devem ser únicas, caso contrário, não fica claro e é ambíguo qual chave deve ir para onde. Você também pode ter problemas com elementos que não se movem como deveriam.
// NÃO RECOMENDADO
var things = [
{ id: '1', name: 'Book' },
{ id: '1', name: 'Cup' },
];
Mithril.js usa um objeto para mapear chaves para índices para saber como corrigir adequadamente fragmentos com chave. Quando você tem uma chave duplicada, não fica claro para onde esse elemento se moveu e, portanto, Mithril.js se comportará de maneira inesperada na atualização, especialmente se a lista foi alterada. Chaves distintas são necessárias para que Mithril.js conecte adequadamente nós antigos para novos, então você deve escolher algo localmente único para usar como chave.
Usando Objetos para Chaves
As chaves para itens de fragmento são tratadas como chaves de propriedade. Coisas como esta não funcionarão como você imagina.
// NÃO RECOMENDADO
things.map(function (thing) {
return m(Thing, { key: thing, thing: thing });
});
Se você tiver um método toString
nele, ele seria chamado, e você estaria dependente do que ele retornar, possivelmente não percebendo que esse método está sendo chamado. Se você não tiver, todos os seus objetos serão convertidos em string para "[object Object]"
e, portanto, você terá um problema desagradável de chave duplicada.