Skip to content
Mithril.js 2
Main Navigation ガイドAPI

日本語

English
简体中文
繁體中文
Español
Français
Русский
Português – Brasil
Deutsch
한국어
Italiano
Polski
Türkçe
čeština
magyar

日本語

English
简体中文
繁體中文
Español
Français
Русский
Português – Brasil
Deutsch
한국어
Italiano
Polski
Türkçe
čeština
magyar

外観

Sidebar Navigation

はじめに

インストール方法

シンプルなアプリケーション

リソース

JSX

レガシーブラウザでの ES6+ の利用

アニメーション

テスト

例

サードパーティとの統合

パスの取り扱い

主要な概念

Virtual DOM ノード

コンポーネント

ライフサイクルメソッド

Key(キー)

自動再描画システム

その他

フレームワークの比較

v1.x からの移行

v0.2.x からの移行

API

このページの内容

Key(キー) ​

Keyとは? ​

キーは、追跡対象となる要素の一意な識別子です。key 属性を要素、コンポーネント、フラグメント vnodeに追加することで指定でき、以下のように使用します。

javascript
m('.user', { key: user.id }, [
  /* ... */
]);

キーは、いくつかのシナリオで役立ちます。

  • モデルデータや状態を持つデータをレンダリングする場合、キーはローカルな状態を正しいサブツリーに関連付けるために必要です。
  • CSS を使用して複数の隣接するノードを個別にアニメーション化し、それらのいずれかを個別に削除する場合、キーはアニメーションが要素に紐づき、意図せず他のノードにジャンプしないようにするために必要です。
  • サブツリーを強制的に再初期化したい場合は、キーを追加し、再初期化時にそのキーを変更して再描画する必要があります。

Keyの制限 ​

注意: フラグメントの子要素は、key 属性を持つ vnode (キー付きフラグメント) のみ、または key 属性を持たない vnode (キーなしフラグメント) のみを含む必要があります。key 属性は、属性をサポートする vnode、つまり要素、コンポーネント、フラグメント vnode にのみ指定できます。null、undefined、文字列などの vnode は属性を持つことができないため、key 属性を持つことができず、キー付きフラグメントで使用できません。

つまり、[m(".foo", {key: 1}), null] や ["foo", m(".bar", {key: 2})] は機能しませんが、[m(".foo", {key: 1}), m(".bar", {key: 2})] や [m(".foo"), null] は機能します。この制限に違反すると、エラーメッセージが表示されます。

ビューのリストでのモデルデータの関連付け ​

リスト、特に編集可能なリストをレンダリングする際、編集可能なTODOなどを扱うことはよくあります。これらには状態と識別子があり、Mithril.js にそれらを追跡するために必要な情報を提供する必要があります。

投稿にコメントしたり、報告などの理由で投稿を非表示にすることができる、単純なソーシャルメディアの投稿リストを例に考えます。

javascript
// `User` と `ComposeWindow` は簡略化のため省略
function CommentCompose() {
  return {
    view: function (vnode) {
      var post = vnode.attrs.post;
      return m(ComposeWindow, {
        placeholder: 'コメントを書いてください...',
        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);
            },
          },
          "非表示にする"
        )
      );
    },
  };
}

function PostCompose() {
  return {
    view: function (vnode) {
      var comment = vnode.attrs.comment;
      return m(ComposeWindow, {
        placeholder: '投稿を書いてください...',
        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,
            ' コメント',
            post.commentCount === 1 ? '件' : '件'
          ),
          m(
            'a.post-hide',
            {
              onclick: function () {
                Model.hidePost(post).then(m.redraw);
              },
            },
            "非表示にする"
          )
        ),
        showComments
          ? m(
              '.post-comments',
              comments == null
                ? m('.comment-list-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', 'フィード'),
        posts == null
          ? m('.post-list-loading', 'ロード中...')
          : m(
              '.post-view',
              m(PostCompose),
              m(
                '.post-list',
                posts.map(function (post) {
                  return m(Post, { post: post });
                })
              )
            )
      );
    },
  };
}

多くの機能がカプセル化されていますが、ここでは次の2点に焦点を当てます。

javascript
// `Feed` コンポーネント内
m(
  '.post-list',
  posts.map(function (post) {
    return m(Post, { post: post });
  })
);

// `Post` コンポーネント内
m(
  '.comment-list',
  comments.map(function (comment) {
    return m(Comment, { comment: comment });
  })
);

これらのコードはそれぞれ、Mithril.js が認識していない関連する状態を持つサブツリーを参照しています (Mithril.js は vnode についてのみ知っており、それ以外の情報は持ちません)。これらのリストにキーを指定せずに放置すると、予期しない動作が発生する可能性があります。この例では、「N件のコメント」をクリックしてコメントを表示し、下部のコメント作成ボックスに入力してから、その上の投稿の「非表示にする」をクリックしてみてください。模擬モデルを備えたライブデモで試してみてください。(注:EdgeまたはIEを使用している場合、リンクのハッシュ長のせいで問題が発生する可能性があります。)

期待どおりの動作をする代わりに、非常に混乱して誤った動作をします。開いていたコメントリストが閉じられ、コメントを開いていた投稿の次の投稿が、コメントをすでにロードしたと認識しているにもかかわらず、「ロード中...」と表示され続けます。これは、コメントが遅延ロードされ、毎回同じコメントが渡されると想定しているためです (ここでは比較的健全に聞こえます)。しかし、実際にはそうではありません。

これは、Mithril.js がキーなしフラグメントを差分適用する方法、つまり非常に単純な方法で反復的に 1 つずつ差分適用することに起因します。したがって、この場合、差分は次のようになります。

  • Before: A, B, C, D, E
  • Patched: A, B, C -> D, D -> E, E -> (削除)

そして、コンポーネントは同じままであるため (常に Comment)、属性のみが変更され、置き換えられることはありません。

このバグを修正するには、キーを追加するだけで、Mithril.js は必要な場合に状態を移動することを認識します。以下は修正済みの動作例です。

javascript
// `Feed` コンポーネント内
m(
  '.post-list',
  posts.map(function (post) {
    return m(Post, { key: post.id, post: post });
  })
);

// `Post` コンポーネント内
m(
  '.comment-list',
  comments.map(function (comment) {
    return m(Comment, { key: comment.id, comment: comment });
  })
);

コメントに関しては、この場合はキーなしでも技術的には機能しますが、ネストされたコメントや編集機能などを追加すると同様に機能しなくなり、キーを追加する必要が生じます。

アニメーションオブジェクトのコレクションを不具合なく管理する ​

リストやボックスなどのアニメーションを実装したい場合があります。まずは、以下の簡単なコードから見ていきましょう。

javascript
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')
            );
          })
        ),
      ];
    },
  };
}

これは一見問題なさそうに見えますが、ライブの例で試してみてください。この例では、クリックしていくつかのボックスを作成し、ボックスを選択して、そのサイズの変化を観察してください。サイズとスピンは、グリッド内の位置ではなく、ボックス自体(色で区別される)に関連付けられるべきです。しかし実際には、サイズが突然大きくなることがあり、場所によって一定に保たれません。これは、各要素に一意な識別子を設定する必要があることを示しています。

この場合、一意なキーを与えるのは簡単です。読み込むたびにインクリメントするカウンターを作成するだけです。

javascript
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')
            );
          })
        ),
      ];
    },
  };
}

修正されたデモで、動作の違いを確認してみてください。

単一子要素のキー付きフラグメントを使用したビューの再初期化 ​

モデルなどのステートフルなエンティティを扱う場合、モデルのビューをキー付きでレンダリングすると便利なことがよくあります。次のようなレイアウトを考えてみましょう。

javascript
function Layout() {
  // ...
}

function Person() {
  // ...
}

m.route(rootElem, '/', {
  '/': Home,
  '/person/:id': {
    render: function () {
      return m(Layout, m(Person, { id: m.route.param('id') }));
    },
  },
  // ...
});

Personコンポーネントは、おそらく次のような実装になるでしょう。

javascript
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)
        // ...
      );
    },
  };
}

たとえば、このコンポーネントから他の人にリンクする方法を追加したとします。具体的には、「マネージャー」フィールドを追加する例を考えます。

javascript
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
          )
        )
        // ...
      );
    },
  };
}

人の ID が 1 で、マネージャーの ID が 2 だとすると、/person/1 から /person/2 に切り替わった場合、同じルートにとどまります。しかし、ルートリゾルバー render メソッド を使用しているため、Person コンポーネントに渡される id が "1" から "2" に変わるだけです。この場合、Person コンポーネント自体は変更されていないため、再初期化されません。これは問題です。新しいユーザー情報が取得されないからです。ここでキーが役立ちます。ルートリゾルバーを次のように変更することで修正できます。

javascript
m.route(rootElem, '/', {
  '/': Home,
  '/person/:id': {
    render: function () {
      return m(
        Layout,
        // 後で他の要素を追加する場合に備えて、配列でラップします。
        // 注意点:フラグメント内では、すべての子要素にキーを設定するか、キーを一切設定しないかのどちらかにする必要があります。
        [m(Person, { id: m.route.param('id'), key: m.route.param('id') })]
      );
    },
  },
  // ...
});

よくある落とし穴 ​

key を使用する際によくある間違いがいくつかあります。それらがなぜうまくいかないのかを理解するために、いくつか紹介します。

key 付き要素のラッピング ​

以下の2つのコードは、同じようには動作しません。

javascript
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 })]);
});

最初の例では、key が User コンポーネントに設定されていますが、users.map(...) で生成される外側のフラグメントには key が設定されていません。このように key 付きの要素をラップしても効果はなく、リストが変更されるたびに不要なリクエストが発生したり、内部のフォーム入力の状態が失われたりするなど、さまざまな問題が起こる可能性があります。結果として生じる動作は、投稿リストの壊れた例と似ていますが、状態が破損することはありません。

2番目の例では、.wrapper 要素に key がバインドされているため、外側のフラグメントが key 付きになります。これは意図した通りの動作であり、ユーザーを削除しても、他のユーザーインスタンスの状態に問題は発生しません。

コンポーネント内に key を配置する ​

人物の例で、代わりに次のように記述したと仮定しましょう。

javascript
// 回避
function Person(vnode) {
  var personId = vnode.attrs.id;
  // ...

  return {
    view: function () {
      return m.fragment(
        { key: personId }
        // 以前に view に記述していたもの
      );
    },
  };
}

これはうまくいきません。key はコンポーネント全体ではなく、view にのみ適用されるため、コンポーネントの再初期化が起こらず、期待どおりにデータを再取得できません。

代わりに、コンポーネント自体の中ではなく、コンポーネントを 使用する vnode に key を配置する、そこで使用されている解決策を推奨します。

javascript
// 推奨
return [m(Person, { id: m.route.param('id'), key: m.route.param('id') })];

不必要に要素に key を設定する ​

key そのものが ID であるという誤解がよくあります。Mithril.js では、フラグメントの子要素はすべて key を持つか、すべて key を持たないかのどちらかでなければならず、このルールに違反するとエラーが発生します。次のレイアウトがあるとします。

javascript
m('.page', m('.header', { key: 'header' }), m('.body'), m('.footer'));

これは明らかにエラーになります。.header には key があり、.body と .footer には key がないためです。しかし、重要なのは、これには key は必要ないということです。このような場合に key を使っていると感じたら、key を追加するのではなく、削除することが解決策になります。本当に、本当に 必要な場合だけ追加してください。確かに、DOM ノードにはそれぞれ ID がありますが、Mithril.js はそれらを正しくパッチするために、必ずしも ID を追跡する必要はありません。ほとんどの場合、追跡は不要です。各エントリに、モデル、コンポーネント、または DOM 自体にあるかどうかに関わらず、Mithril.js 自体が追跡しない何らかの関連付けられた状態があるリストでのみ、key が必要になります。

最後に、静的な key は避けてください。それらは常に不要です。key 属性を計算していない場合は、おそらく何か間違ったことをしています。

分離された単一の key 付き要素が本当に必要な場合は、単一の子を持つ key 付きフラグメントを使用してください。これは、[m("div", {key: foo})] のように、key 付き要素である単一の子を持つ配列です。

key の型の混合 ​

key はオブジェクトのプロパティ名として読み取られます。これは、1 と "1" が同一として扱われることを意味します。できる限り、異なる型の key を混在させないようにしてください。そうすると、key が重複し、予期しない動作が発生する可能性があります。

javascript
// 回避
var things = [
  { id: '1', name: 'Book' },
  { id: 1, name: 'Cup' },
];

どうしても必要で、制御できない場合は、型を示すプレフィックスを使用して、それらを区別してください。

javascript
things.map(function (thing) {
  return m(
    '.thing',
    { key: typeof thing.id + ':' + thing.id }
    // ...
  );
});

穴のある key 付き要素の非表示 ​

null、undefined、およびブール値のような穴は、key のない vnode と見なされます。そのため、次のようなコードは機能しません。

javascript
// 回避
things.map(function (thing) {
  return shouldShowThing(thing)
    ? m(Thing, { key: thing.id, thing: thing })
    : null;
});

代わりに、リストを返す前にフィルタリングすると、Mithril.js は正しい処理を行います。ほとんどの場合、Array.prototype.filter がまさに必要なものであり、ぜひ試してみてください。

javascript
// 推奨
things
  .filter(function (thing) {
    return shouldShowThing(thing);
  })
  .map(function (thing) {
    return m(Thing, { key: thing.id, thing: thing });
  });

重複する key ​

フラグメント項目の key は 一意 でなければなりません。そうでない場合、どの key がどこに行くべきかが不明確で曖昧になります。また、要素が意図したとおりに移動しないという問題が発生する可能性もあります。

javascript
// 回避
var things = [
  { id: '1', name: 'Book' },
  { id: '1', name: 'Cup' },
];

Mithril.js は、key をインデックスにマッピングするために内部的に空のオブジェクトを使用し、key 付きフラグメントを効率的にパッチするための情報を管理しています。key が重複している場合、要素の移動先が特定できなくなり、Mithril.js は正常に動作しなくなる可能性があります。特にリストが変更された場合、予期せぬ更新結果になることがあります。Mithril.js が古いノードを新しいノードに適切に接続するには、一意の key が必要です。したがって、ローカルで一意なものを key として使用する必要があります。

key にオブジェクトを使用する ​

フラグメント項目の key は、プロパティキーとして扱われます。次のようなものは、期待通りには動作しません。

javascript
// 回避
things.map(function (thing) {
  return m(Thing, { key: thing, thing: thing });
});

オブジェクトに toString メソッドがある場合、それが暗黙的に呼び出され、その戻り値が key として使用されます。toString メソッドが呼び出されていることに気づかない場合もあります。toString メソッドがない場合、すべてのオブジェクトは "[object Object]" という文字列に変換されるため、重複する key の問題が発生します。

Pager
前のページライフサイクルメソッド
次のページ自動再描画システム

MITライセンス の下で公開されています。

Copyright (c) 2024 Mithril Contributors

https://mithril.js.org/keys.html

MITライセンス の下で公開されています。

Copyright (c) 2024 Mithril Contributors