☑️

HTMLのAttributeとPropertyの違いを知らなかった話

2024/02/13に公開

「知らなかった」と書いていますが、まだ違いをしっかりと理解しているわけではないので、以下の内容は正確性に欠けるかもしれません。が、ひとまず自分の理解を文字に起こそうと思います。

そもそもの発端

Reactっぽいフロントエンドライブラリを作ったので、チェックボックスの動作をReactっぽく書こうとしたらうまくいかなかった、ということがありました。

どういうことかというと、

以下のようなコードを書くことができるのですが、チェックの挙動が思った通りになりませんでした。

const App = () => {
  const store = useStore({ checked: false });
  return (
    <div>
      <button onclick={() => (store.checked = !store.checked)}>check</button>
      <input
        type="checkbox"
        checked={store.checked}
        onchange={() => (store.checked = !store.checked)}
      />
    </div>
  );
}

ボタンをクリックすると、チェックボックスのチェックを入れたり外したりできます。また、チェックボックスをクリックしてもチェックを入れたり外したりできます。
が、チェックボックスをクリックすると、ボタンを押してもチェック状態が変わらなくなります。ステートはきちんと変わっているし、HTML上を見てもボタンを押す度にcheckedがついたり消えたりします。


ボタンをクリックすると、ちゃんと反応する。チェックボックスをクリックしても、ちゃんと反応する。
でも、その次にボタンをクリックすると、反応しなくなる(checkedはつくのに)!という動画

※動画はループしています。

なぜなのか

属性(Attribute)とプロパティ(Property)の違いを知らなかったことからきている問題でした。

チェックボックスがチェックされているか否かは、属性 checked で指定できます。また、input要素のプロパティ checked にbooleanを渡すと選択状態、非選択状態を切り替えることができます。

この挙動をみると、属性 checked とプロパティ checked が同じもののように見えます。が、実は属性はチェック状態の初期状態を渡すことができるだけであって、実際にはプロパティの方が本体なのです。プロパティを直接書き換えても属性の方には影響はありません(checkedがついている状態でinput.checked = falseにしても、checkedは消えない)。

チェックボックスはcheckedという属性・プロパティについて以下のような挙動をします。

  • 属性 checked をセットすることで、チェック状態にすることができる。
  • プロパティに触れていない状態ならば、属性 checked を切り替えることでチェック状態を切り替えることができる。
  • クリックしてチェックを入れると、以後、属性 checked をいくら操作してもチェック状態を切り替えることができなくなる。
  • プロパティに直接booleanをセットしてもクリックと同等の効果を与えることができる。

これらの挙動から以下の仕様だと推測できます。

  • プロパティを操作していないときは属性に従う。
  • プロパティを操作(クリックするか、直接プロパティに値を代入)すると、プロパティの値が優先され、属性の状態は無視される。

つまり、属性を変更することで状態を変更できるがプロパティには勝てない、というわけです。

最初のコードがなぜ動かなかったのか

これに関してはライブラリの内部コードを知らねばどうにもなりませんが、プロパティではなく属性のみをいじくる実装になっていたのですからしょうがありません。端折っていますが以下のような実装になっていたと思っていただければよいと思います。

// 再レンダリングする際に、属性を更新する関数
function updateAttributes(el: HTMLElement, attributes: { [key: string]: any }) {
  Object.entries(attributes).forEach(([key, value]) => {
    // 値がbooleanの場合は属性をつけたり外したりしたい
    if (typeof value === "boolean") {
      if (value) el.setAttribute(key);
      else el.removeAttribute(key);
    } else {
      el.setAttribute(key, value);
    }
  });
}

このコードだと、 <input type="checkbox" checked={true} /> のときに、チェック状態にできますが、ユーザーがクリックしたあとでチェック状態を切り替えることはできません。

なぜ気づかなかったのか

サンプルコードでチェック状態が切り替わったので喜んでいたのですが、実際にはチェックボックスがチェックボックスの務めを果たしていただけ(クリックされたらプロパティの状態を反転させる)であり、外から渡していた属性 checked はなんの影響も与えていなかったというわけです。

今回、ボタンをクリックしてチェック状態を切り替えられるようにしたところはじめて気づきました。

修正方法

HTMLElementで同名の属性とプロパティがあるとき、属性をセットせずにプロパティを更新すればよいでしょう。

まとめ

HTMLElementの属性とプロパティの違いを知らないままフロントエンドライブラリを作ったら、バグを生み出してしまった、というお話でした。
Reactをはじめとしたフロントエンドフレームワークはこういう細かな仕様にもきちんと対応していてすごいですね。

以上です。よろしくお願いします。

※追記

記事を書いた後にほんまか?と思って調べてみたのですが、属性とプロパティの関係は以下のようになっているようです。

  • HTML要素に属性を設定すると、その属性がそのHTML要素にとって標準の属性であれば、同名のプロパティを生成する
    • 例えば、 id は全てのHTML要素において標準なので、属性 id を設定するとプロパティ id にアクセスできる
    • プロパティを更新すると、属性も更新される
    • 標準の属性でなければ、プロパティの生成は行われないし、同期も行われない
  • 特別な属性では、プロパティと属性の同期が行われない場合がある(例外)
    • input 要素の value などがそれ
    • 今回のチェックボックスの checked もそれ
    • 詳しくはHTMLの仕様に書いてある
    • プロパティが設定されると属性が無視されるようになる仕様はDirty checkednessという

つまり、この記事で書いた話は特別な属性の話で、通常は属性 = プロパティとしてよいようですね。

参考: https://ja.javascript.info/dom-attributes-and-properties

Discussion