mithril
Version:
A framework for building brilliant applications
271 lines (256 loc) • 13.6 kB
HTML
<head>
<meta charset=UTF-8>
<title> Keys - Mithril.js</title>
<link href="https://fonts.googleapis.com/css?family=Open+Sans" rel=stylesheet>
<link href=style.css rel=stylesheet>
<link rel=icon type=image/png sizes=32x32 href=favicon.png>
<meta name=viewport content="width=device-width,initial-scale=1">
</head>
<body>
<header>
<section>
<a class=hamburger href=javascript:;>≡</a>
<h1><img src=logo.svg> Mithril <small>2.0.3</small></h1>
<nav>
<a href=index.html>Guide</a>
<a href=api.html>API</a>
<a href=https://gitter.im/MithrilJS/mithril.js>Chat</a>
<a href=https://github.com/MithrilJS/mithril.js>GitHub</a>
</nav>
</section>
</header>
<main>
<section>
<h1 id=keys><a href=#keys>Keys</a></h1>
<ul>
<li>Getting Started<ul>
<li><a href=index.html>Introduction</a></li>
<li><a href=installation.html>Installation</a></li>
<li><a href=simple-application.html>Tutorial</a></li>
<li><a href=learning-mithril.html>Learning Resources</a></li>
<li><a href=support.html>Getting Help</a></li>
</ul>
</li>
<li>Resources<ul>
<li><a href=jsx.html>JSX</a></li>
<li><a href=es6.html>ES6+ on legacy browsers</a></li>
<li><a href=animation.html>Animation</a></li>
<li><a href=testing.html>Testing</a></li>
<li><a href=examples.html>Examples</a></li>
<li><a href=integrating-libs.html>3rd Party Integration</a></li>
<li><a href=paths.html>Path Handling</a></li>
</ul>
</li>
<li>Key concepts<ul>
<li><a href=vnodes.html>Vnodes</a></li>
<li><a href=components.html>Components</a></li>
<li><a href=lifecycle-methods.html>Lifecycle methods</a></li>
<li><strong><a href=keys.html>Keys</a></strong><ul>
<li><a href=#what-are-keys>What are keys</a></li>
<li><a href=#how-to-use>How to use</a><ul>
<li><a href=#single-child-keyed-fragments>Single-child keyed fragments</a></li>
</ul>
</li>
<li><a href=#debugging-key-related-issues>Debugging key related issues</a></li>
</ul>
</li>
<li><a href=autoredraw.html>Autoredraw system</a></li>
</ul>
</li>
<li>Social<ul>
<li><a href=https://github.com/MithrilJS/mithril.js/wiki/JOBS>Mithril Jobs</a></li>
<li><a href=contributing.html>How to contribute</a></li>
<li><a href=credits.html>Credits</a></li>
<li><a href=code-of-conduct.html>Code of Conduct</a></li>
</ul>
</li>
<li>Misc<ul>
<li><a href=framework-comparison.html>Framework comparison</a></li>
<li><a href=change-log.html>Change log/Migration</a></li>
<li><a href=https://mithril.js.org/archive/v1.1.6/ >v1 Documentation</a></li>
<li><a href=https://mithril.js.org/archive/v0.2.5/ >v0.2 Documentation</a></li>
</ul>
</li>
</ul>
<hr>
<h3 id=what-are-keys><a href=#what-are-keys>What are keys</a></h3>
<p>Keys are a mechanism that allows re-ordering DOM elements within a NodeList, and mapping specific data items in a list to the respective DOM elements that are derived from them, as the data items move within the list.</p>
<p>In other words, a <code>key</code> is a way of saying "this DOM element is for the data object with this id".</p>
<p>Typically, a <code>key</code> property should be the unique identifier field of the objects in the data array.</p>
<pre><code class=language-javascript>var users = [
{id: 1, name: "John"},
{id: 2, name: "Mary"},
]
function userInputs(users) {
return users.map(function(u) {
return m("input", {key: u.id}, u.name)
})
}
m.render(document.body, userInputs(users))</code></pre>
<p>Having a key means that if the <code>users</code> array is shuffled and the view is re-rendered, the inputs will be shuffled in the exact same order, so as to maintain correct focus and DOM state.</p>
<hr>
<h3 id=how-to-use><a href=#how-to-use>How to use</a></h3>
<p>A common pattern is to have data comprised of an array of objects and to generate a list of vnodes that map to each object in the array. For example, consider the following code:</p>
<pre><code class=language-javascript>var people = [
{id: 1, name: "John"},
{id: 2, name: "Mary"},
]
function userList(users) {
return users.map(function(u) {
return m("button", u.name) // <button>John</button>
// <button>Mary</button>
})
}
m.render(document.body, userList(people))</code></pre>
<p>Let's suppose the <code>people</code> variable was changed to this:</p>
<pre><code class=language-javascript>people = [{id: 2, name: "Mary"}]</code></pre>
<p>The problem is that from the point of view of the <code>userList</code> function, there's no way to tell if it was the first object that was removed, or if it was the second object that was removed <em>in addition to the first object's properties being modified</em>. If the first button was focused and the rendering engine removes it, then focus goes back to <code><body></code> as expected, but if the rendering engine removes the second button and modifies the text content of the first, then the focus will be on the wrong button after the update.</p>
<p>Worse still, if there were stateful jQuery plugins attached to these buttons, they could potentially have incorrect internal state after the update.</p>
<p>Even though in this particular example, we humans intuitively guess that the first item in the list was the one being removed, it's actually impossible for a computer to automatically solve this problem for all possible inputs.</p>
<p>Therefore, in the cases when a list of vnodes is derived from a dynamic array of data, you should add a <code>key</code> property to each virtual node that maps to a uniquely identifiable field in the source data. This will allow Mithril to intelligently re-order the DOM to maintain each DOM element correctly mapped to its respective item in the data source.</p>
<pre><code class=language-javascript>function correctUserList(users) {
return users.map(function(u) {
return m("button", {key: u.id}, u.name)
})
}</code></pre>
<h4 id=single-child-keyed-fragments><a href=#single-child-keyed-fragments>Single-child keyed fragments</a></h4>
<p>Sometimes, you might want to reinitialize a component on command. You can use the common pattern of a single-child keyed fragment where you change the key to destroy and reinitialize the element.</p>
<pre><code class=language-javascript>function ResettableToggle() {
var toggleKey = false
function reset() {
toggleKey = !toggleKey
}
return {
view: function() {
return [
m("button", {onclick: reset}, "Reset toggle"),
[m(Toggle, {key: toggleKey})]
]
}
}
}</code></pre>
<p>You can also bind it to a known identity, for things like item views where you need to fetch a remote resource based on an ID. It's usually simpler than implementing all the logic to diff the ID and re-fetch a resource if it changes.</p>
<pre><code class=language-javascript>function Page() {
return {
view: function() {
return m(Layout, [
[m(ItemView, {key: m.route.param("id")})],
])
}
}
}</code></pre>
<hr>
<h3 id=debugging-key-related-issues><a href=#debugging-key-related-issues>Debugging key related issues</a></h3>
<p>Keys can cause confusing issues if they are misunderstood. A typical symptom of key related issues is that application state appears to become corrupted after a few user interactions (usually involving a deletion).</p>
<h4 id=avoid-wrapper-elements-around-keyed-elements><a href=#avoid-wrapper-elements-around-keyed-elements>Avoid wrapper elements around keyed elements</a></h4>
<p>Keys must be placed on the virtual node that is an immediate child of the array. This means that if you wrap the <code>button</code> in an <code>div</code> in the example above, the key must be moved to the <code>div</code>.</p>
<pre><code class=language-javascript>// AVOID
users.map(function(u) {
return m("div", [ // key should be in `div`
m("button", {key: u.id}, u.name)
])
})</code></pre>
<h4 id=avoid-hiding-keys-in-component-root-elements><a href=#avoid-hiding-keys-in-component-root-elements>Avoid hiding keys in component root elements</a></h4>
<p>If you refactor the code and make a user component, the key must be moved out of the component and put on the component itself, since it is now the immediate child of the array.</p>
<pre><code class=language-javascript>// AVOID - doesn't work
var User = {
view: function(vnode) {
return m("div", { key: vnode.attrs.user.id }, [
m(Button, vnode.attrs.user.name)
])
}
}
// PREFER
users.map(function(u) {
return m(User, { key: u.id, user: u }) // key should be here, not in component
})</code></pre>
<h4 id=avoid-wrapping-keyed-elements-in-arrays><a href=#avoid-wrapping-keyed-elements-in-arrays>Avoid wrapping keyed elements in arrays</a></h4>
<p>Arrays are <a href=vnodes.html>vnodes</a>, and therefore keyable. You should not wrap arrays around keyed elements outside <a href=#single-child-keyed-fragments>single-child keyed fragments</a>.</p>
<pre><code class=language-javascript>// AVOID
users.map(function(u) {
return [ // fragment is a vnode, and therefore keyable
m("button", {key: u.id}, u.name)
]
})
// PREFER
users.map(function(u) {
return m("button", {key: u.id}, u.name)
})
// PREFER
users.map(function(u) {
return m.fragment({key: u.id}, m("button", u.name))
})</code></pre>
<h4 id=avoid-variable-types><a href=#avoid-variable-types>Avoid variable types</a></h4>
<p>Keys must be strings if present or they will be cast to strings if they are not. Therefore, <code>"1"</code> (string) and <code>1</code> (number) are considered the same key.</p>
<p>You should use either strings or numbers as keys in one array, but not mix both.</p>
<pre><code class=language-javascript>// AVOID
var things = [
{id: "1", name: "Book"},
{id: 1, name: "Cup"},
]</code></pre>
<h4 id=avoid-mixing-keyed-and-non-keyed-vnodes-in-the-same-array><a href=#avoid-mixing-keyed-and-non-keyed-vnodes-in-the-same-array>Avoid mixing keyed and non-keyed vnodes in the same array</a></h4>
<p>An array of vnodes must have only keyed vnodes or non-keyed vnodes, but not both. In fact, this will cause an error to be thrown to remind you to not do it. So don't do it!</p>
<pre><code class=language-javascript>// AVOID
m("div", [
m("div", "a"),
m("div", {key: 1}, "b"),
])</code></pre>
<p>If using keys, use a unique key for every element.</p>
<pre><code class=language-javascript>// PREFER
m("div", [
m("div", {key: 0}, "a"),
m("div", {key: 1}, "b"),
])</code></pre>
<p>If you need to mix them, create a nested array.</p>
<pre><code class=language-javascript>// PREFER
m("div", [
m("div", "a"),
[
m("div", {key: 1}, "b"),
]
])</code></pre>
<p>Note that <code>null</code>s, <code>undefined</code>s and booleans are considered unkeyed nodes. If you want the keyed equivalent, use <code>m.fragment({key: ...}, [])</code> which is a keyed empty fragment.</p>
<pre><code class=language-javascript>// AVOID
m("div", [
a ? m("div", {key: 1}, "a") : null,
m("div", {key: 2}, "b"),
])
// PREFER
m("div", [
a ? m("div", {key: 1}, "a") : m.fragment({key: 1}, []),
m("div", {key: 2}, "b"),
])</code></pre>
<h4 id=avoid-passing-model-data-directly-to-components-if-the-model-uses-key-as-a-data-property><a href=#avoid-passing-model-data-directly-to-components-if-the-model-uses-key-as-a-data-property>Avoid passing model data directly to components if the model uses <code>key</code> as a data property</a></h4>
<p>The <code>key</code> property may appear in your data model in a way that conflicts with Mithril's key logic. For example, a component may represent an entity whose <code>key</code> property is expected to change over time. This can lead to components receiving the wrong data, re-initialize, or change positions unexpectedly. If your data model uses the <code>key</code> property, make sure to wrap the data such that Mithril doesn't misinterpret it as a rendering instruction:</p>
<pre><code class=language-javascript>// Data model
var users = [
{id: 1, name: "John", key: 'a'},
{id: 2, name: "Mary", key: 'b'},
]
// Later on...
users[0].key = 'c'
// AVOID
users.map(function(user){
// The component for John will be destroyed and recreated
return m(UserComponent, user)
})
// PREFER
users.map(function(user){
// Key is specifically extracted: data model is given its own property
return m(UserComponent, {key: user.id, model: user})
})</code></pre>
<hr>
<small>License: MIT. © Leo Horie.</small>
</section>
</main>
<script src=https://cdnjs.cloudflare.com/ajax/libs/prism/1.6.0/prism.min.js defer></script>
<script src=https://cdnjs.cloudflare.com/ajax/libs/prism/1.6.0/components/prism-jsx.min.js defer></script>
<script src=https://unpkg.com/mithril@2.0.3/mithril.js async></script>
<script>
document.querySelector(".hamburger").onclick = function() {
document.body.className = document.body.className === "navigating" ? "" : "navigating"
document.querySelector("h1 + ul").onclick = function() {
document.body.className = ''
}
}
</script>