🎉

宣言的UIと命令的UIとは?コードで見る違い

2023/04/24に公開

はじめに

宣言的UIを理解するためにまずは「宣言」という言葉から見ていこうと思います。

その後により理解を深めるために宣言的UIが登場する前に使われていた命令的UIを説明します。宣言的UIは命令的UIの問題点を解決するために登場したものなので、両者の違いを見ると宣言的UIの素晴らしさが伝わると思います。

宣言という言葉の意味から宣言的UIを考えてみる

Googleで検索したところ次のような言葉の定義が出てきました。

広く一般にむかっていうこと。個人や団体などが、その意思・意見・方針などを、広く外部に表明すること。

これだと少し大げさなので上記のフォーマットをあまり崩さずに言葉を換えます。

コードを読むすべての人に対して、マークアップがどういったUIになるか表明すること

※コードを読むすべての人に対して(広く外部に)、マークアップがどういったUIになるか(意思・意見・方針などを)表明すること

つまりマークアップを見ただけで状態の変化も含めて、どのようなUIになるか わかるようなUIを宣言的UIといいます。

Reactで見る宣言的UI

宣言的UIを実現するためにReactではUIの変化する部分をstateという変数を使って定義します。

import { useState } from "react";
import "./styles.css";

export default function App() {
  const [count] = useState(0);
  return (
    <div className="App">
      <h1>カウントアプリ</h1>
      <p>{count}</p>
    </div>
  );
}

UIは以下のようになります。

前項で書いた通り、実際にブラウザに表示されるUIがマークアップと一致しています。動的に変化する部分はcount変数にすることで判断できますね。

ではこのカウント0から1に変化させるとUIの構造はどうなるでしょうか。
useStateのset関数をつかってカウントアップを実装します。

export default function App() {
  const [count, setCount] = useState(0);
  return (
    <div className="App">
      <h1>カウントアプリ</h1>
      <p>{count}</p>
      <button type="button" onClick={() => setCount((c) => c + 1)}>
        count up
      </button>
    </div>
  );
}

カウントアップを実装したことでボタンが増えました。
ボタンを押下するとsetCountが呼ばれてcount変数が更新されます。3回押すと以下のようになりました。

カウントアップを実装してもマークアップとUIが一致しており、マークアップからどのようなUIになるか想像ができます。
あれでも関数がUIに入ってないか?と思うかもしれません。これは分けて書くことができます。

export default function App() {
  const [count, setCount] = useState(0);

  // UIから分離した
  const handleCountupClick = () => {
    setCount((c) => c + 1);
  };

  return (
    <div className="App">
      <h1>カウントアプリ</h1>
      <p>{count}</p>
      <button type="button" onClick={handleCountupClick}>
        count up
      </button>
    </div>
  );
}

このように宣言的UIを実現するためにReactでは、UIとロジックのつながりがほとんどない状態となっており、ReactがUIを直接操作することはありません。UIが状態の変化を監視してUIを変化させています。

(useRefを使ってDOM操作することがありますが、あまり多くないと思います。forcusしたりscrollするときに使うイメージです。詳細はReactの公式Docsを参照してみてください。)

Javaで見る宣言的UI

Javaでも宣言的なUIが見られます。JavaではThymeleafというツールを使ってHTMLの中身をJavaで書き換えます。HTMLを次のように書きます。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <body>
        <h1>カウントアップ</h1>
	<p th:text="${count}"></p>
    </body>
</body>
</html>

かなりReactに似ていますね。

命令的なUI

例えば以下のような命令的UIがあります。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <form id="form">
    <label>First name:
      <b id="nameText">Jane</b>
      <input id="nameInput" value="Jane">
    </label>
    <button type="submit" id="editButton">Edit Profile</button>
    <p>
      <i id="helloText">Hello, Jane</i>
    </p>
  </form>
</body>
  
</html

UIの更新ロジックは以下の通りです。

function handleFormSubmit(e) {
  e.preventDefault();
  if (editButton.textContent === 'Edit Profile') {
    editButton.textContent = 'Save Profile';
    hide(nameText);
    show(nameInput);
  } else {
    editButton.textContent = 'Edit Profile';
    hide(nameInput);
    show(nameText);
  }
}

function handleNameChange() {
  nameText.textContent = nameInput.value;
  helloText.textContent = (
    'Hello ' +
    nameInput.value + '!'
  );
}

function hide(el) {
  el.style.display = 'none';
}

function show(el) {
  el.style.display = '';
}

// DOM要素の取得
let form = document.getElementById('form');
let editButton = document.getElementById('editButton');
let nameInput = document.getElementById('nameInput');
let nameText = document.getElementById('nameText');
let helloText = document.getElementById('helloText');
form.onsubmit = handleFormSubmit;
nameInput.oninput = handleNameChange;

なんとなくわかりづらいのが伝わるでしょうか。
一旦、コードを読んでみて何をしているコードなのか予想してみてください。



ではこれをReactで書き直してみます。

import { useState } from "react";
export default function App() {
  const [name, setName] = useState("");
  const [edit, setEdit] = useState(true);
  const handleNameChange = (e) => {
    setName(e.target.value);
  };
  const handleSubmit = (e) => {
    e.preventDefault();
    setEdit((e) => !e);
  };
  return (
    <form onSubmit={handleSubmit}>
      <label>
        First name:
        {edit ? (
          <input value={name} onChange={handleNameChange} />
        ) : (
          <b>{name}</b>
        )}
      </label>
      <button type="submit">Edit Profile</button>
      <p>
        <i id="helloText">{`Hello, ${name}`}</i>
      </p>
    </form>
  );
}

Reactのほうがコード量が少なく、かつ読みやすいですね。

※以下のような要件のアプリでした。
・名前を入力すると「Hello <入力した名前>!」が表示される
・Edit Profileボタンを押下すると入力フォームが消えて、入力した名前を太字で表示する

命令的UIと宣言的UIの違いはなんでしょうか。命令的UIは宣言的UIと同じようにマークアップとUIが一致しているように見えます。しかし宣言的UIと違って命令的UIでは、どこが変化するのかがわからないので、マークアップが最終的にどういったUIになるのかわかりません。

命令的UIにおける変更では、ロジックだけではなくUIに対しても細かく命令を出していく必要があります。なぜかというとReactではuseState APIの戻り値を使って動的に変化する部分をUIに埋め込むことができました。さらに変更を加えるためのイベントハンドラもUIの中に定義できました。

命令的UIでは上記のような仕組みが存在しないため次のようにコードを書いていく必要があります。

  1. 変更したいタグを取得する
  2. タグに対してイベントハンドラを指定する
  3. イベントハンドラを定義する。
  4. イベントハンドラ内で処理をして、1で取得したタグに値をセット

このようにUIに対して、変更が入るたびに「この値を表示して」と直接命令していくようなUIを命令的UIといいます。

命令的UIではUIが状態を監視していないため、状態の参照・計算・更新をしてさらにUIにそれを伝えないといけません。宣言的UIと違って、状態とUIどちらにも同じような命令の処理をするため、簡単なことをする場合でも複雑になってしまいます。
宣言的UIの図と比べるとロジックから双方向の矢印がUIと状態に向けられています。

まとめ

・宣言的UIとは、コードを読むすべての人に対して、マークアップがどういったUIになるか表明すること(マークアップを見ただけで状態の変化も含めてどういったUIになるかわかるUI)
・Reactでは動的に変化する部分をuseStateを使って表現することで宣言的UIにしている
・宣言的UIではUIは状態を見ているだけ。ロジックはUIを直接変更しない。
・命令的UIでは、変化させる部分をいちいち指示を出さなくてはならない。

Discussion