Mithril 1.1.0

キー


キーとは何か?

キーはノードのリスト内のDOMエレメントを並び替えをサポートする機能です。データ項目のリストを元にしてDOMエレメントを作る時に、そのリストないでの順番の変更をサポートします。

いいかえると、キーは「このDOMエレメントは、このIDを持つデータオブジェクトを表しているデータだ」と説明するための機能です。

通常、キープロパティはデータの配列の中で、ユニークな識別子が格納されているオブジェクトのフィールドを使います。

var users = [
    {id: 1, name: "ジョン"},
    {id: 2, name: "メアリー"},
]

function userInputs(users) {
    return users.map(function(u) {
        return m("input", {key: u.id}, u.name)
    })
}

m.render(document.body, userInputs(users))

キーを持つと、users配列がシャッフルされてビューが再描画されても、inputタグも同じ順序で正しくシャッフルされ、フォーカスとDOMの状態が維持されます。


どのように使うか?

オブジェクトの配列で構成されるデータを元に、配列内の各オブジェクトと対応するvnodeのリストを生成するのが一般的な使用方法です。例えば、次のようなコードについて見てみましょう:

var people = [
    {id: 1, name: "ジョン"},
    {id: 2, name: "メアリー"},
]

function userList(users) {
    return users.map(function(u) {
        return m("button", u.name) // <button>ジョン</button>
                                   // <button>メアリー</button>
    })
}

m.render(document.body, userList(people))

people配列を次のように変更してみましょう:

people = [{id: 2, name: "メアリー"}]

userList関数の立場からこの問題を見ると、果たして最初のオブジェクトが削除されたのか、最初のオブジェクトの要素のプロパティが変更されてから二番目のオブジェクトが削除されたのか、区別がつきません。もし最初のボタンにフォーカスがあった時にレンダリングエンジンがこの要素を削除すると、フォーカスは<body>に戻ることが期待されますが、二番目の要素を削除した上で最初の要素のテキストを修正してしまうと、更新後にフォーカスが間違ったボタンに残ってしまいます。

これらのボタンにステートフルなjQueryプラグインが適用されていた場合には、更新後に内部状態が不正確になる可能性があります。

この例の場合、人間は直感的にリストの最初の項目が削除されたものと推測しますが、すべてのケースに対してこの問題を正しく解決することは不可能です。

動的なデータの配列を元にvnodeのリストを作っている場合には、それぞれの仮想ノードに、元のデータを識別するためのkeyプロパティを追加することで解決することができます。これを設定すると、MithrilはDOMの要素とその元となるデータソースをきちんと関連付けたまま、DOMを賢く並び替えることができるようになります。

function correctUserList(users) {
    return users.map(function(u) {
        return m("button", {key: u.id}, u.name)
    })
}

キーを誤解したまま使うと、分かりにくい問題を引き起こすことがあります。キーにまつわる良くある症状は、ユーザーのインタラクションを何度か実行すると(削除が関係することが多い)、アプリケーションの状態が壊れて見えることです。

キーが付与されたエレメントをラップしない

キーは配列の直下の子要素の仮想nodeに付与する必要があります。もし、上記のサンプルでbuttondivタグでラップしていたのであれば、キーをこの親のdivに移動します。

// 避けるべきコード
users.map(function(u) {
    return m("div", [ // キーは `div` に書かなければならない
        m("button", {key: u.id}, u.name)
    ])
})

コンポーネントのルート要素のキーを隠さない

もしコードをリファクタリングしてボタンをコンポーネント内にまとめた時は、キーをコンポーネントの外側に移動し、コンポーネントとボタンが入れ替わった位置に適切に戻します。

// 避けるべきコード
var Button = {
    view: function(vnode) {
        return m("button", {key: vnode.attrs.id}, u.name)
    }
}

// 望ましいコード
users.map(function(u) {
    return m("div", [
        m(Button, {key: u.id}, u.name) // キーはコンポーネントではなくここに書く
    ])
})

キーが設定されたエレメントを配列でラップしない

配列はvnodeのリストで、これ自身にキーを設定できます。キーを付与したエレメントを配列でラップしてはいけません

// 避けるべきコード
users.map(function(u) {
    return [ // フラグメントはvnodeで、それ自身にキーが設定できる
        m("button", {key: u.id}, u.name)
    ]
})

// 望ましいコード
users.map(function(u) {
    return m("button", {key: u.id}, u.name)
})

// 望ましいコード
users.map(function(u) {
    return m.fragment({key: u.id}, m("button", u.name))
})

型を統一する

キーは文字列でなければなりません。もし文字列でなければ、文字列にキャストされます。そのため、"1"(文字列)と1(数値)は同じキーとみなされます。

ひとつの配列の中では文字列か数値のどちらもキーとして使えますが、混ぜてはいけません。

// 避けるべきコード
var things = [
    {id: "1", name: "本"},
    {id: 1, name: "カップ"},
]

同じ配列で、キー付きの要素とキーなしのvnodeを混ぜない

1つのvnode配列では、キーありのvnodeのみ、あるいはキーなしのvnodeのみのどちらかが許されます。両方を混ぜることはできません。もし混ぜる必要があるのであれば、ネストした配列を作ります。

// 避けるべきコード
m("div", [
    m("div", "a"),
    m("div", {key: 1}, "b"),
])

// 望ましいコード
m("div", [
    m("div", {key: 0}, "a"),
    m("div", {key: 1}, "b"),
])


// 望ましいコード
m("div", [
    m("div", "a"),
    [
        m("div", {key: 1}, "b"),
    ]
])

keyがデータのプロパティとして使用されている時は、モデルデータをコンポーネントに直接渡すのは避ける

keyプロパティがモデルデータに登場すると、Mithrilのキーのロジックとコンフリクトします。例えば、コンポーネントが可変のkeyプロパティを持っているエンティティを表現していたとします。この場合、コンポーネントが間違ったデータを受け取ったり、最初期化されたり、位置が予期せぬ場所に移動したりします。もしデータモデルがkeyという名前のプロパティを持っていたとしても、これをきちんとラップすると、Mithrilがレンダリング用の指示だと間違って解釈することがなくなります。

// データモデル
var users = [
    {id: 1, name: "ジョン", key: 'a'},
    {id: 2, name: "メアリー", key: 'b'},
]

// あとで変更...
users[0].key = 'c'

// 避けるべきコード
users.map(function(user){
    // ジョンに対するコンポーネントは破壊されて再生成されます
    return m(UserComponent, user) 
})

// 望ましいコード
users.map(function(user){
    // キーを明示的に抽出する:データモデルには独自のプロパティが与えられている
    return m(UserComponent, {key: user.id, model: user}) 
})

License: MIT. © Leo Horie.