宣言的 UI

Part 1 · React の世界観

宣言的 UI

「UI は状態の写像である」の意味

React の説明でよく出てくる「宣言的 UI」という言葉。 日本語として硬いので身構えてしまいますが、 言っていることはひとつだけです。

画面をどう作り変えるかではなく、画面がどうあるべきかを書く。

この違いが分かると、React のコードが「なぜあの形をしているのか」に納得がいきます。

手順を書くやり方(命令的)

ログイン状態によって、画面の 3 か所を変えたいとします。 ヘッダーの名前、本文、ボタンの文字です。

手順として書くと、こうなります。

function login() {
  header.textContent = "さとうさん";
  body.textContent = "会員向けの内容が表示されています";
  button.textContent = "ログアウトする";
}

function logout() {
  header.textContent = "ゲスト";
  body.textContent = "ログインすると内容が表示されます";
  button.textContent = "ログインする";
}

間違ってはいません。ただ、同じことを 2 回、逆向きに書いているのが気になります。

そして表示する場所が 1 つ増えるたびに、loginlogout両方に書き足す必要があります。 片方だけ直してしまうと、 「ログアウトしたのにヘッダーだけ名前が残っている」という状態が生まれます。

login-view.tsx

"use client";

import { Button } from "@/components/ui/button";
import { useState } from "react";

export function LoginView() {
  // 画面が持っている状態は、この 1 つだけ
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  return (
    <div className="flex flex-col gap-4">
      <div className="flex items-center justify-between rounded-md border px-3 py-2">
        <span className="text-muted-foreground">ヘッダー</span>
        <span className="font-semibold">
          {isLoggedIn ? "さとうさん" : "ゲスト"}
        </span>
      </div>

      <div className="rounded-md border px-3 py-2">
        {isLoggedIn
          ? "会員向けの内容が表示されています"
          : "ログインすると内容が表示されます"}
      </div>

      <Button size="sm" onClick={() => setIsLoggedIn(!isLoggedIn)}>
        {isLoggedIn ? "ログアウトする" : "ログインする"}
      </Button>
    </div>
  );
}

あるべき姿を書くやり方(宣言的)

React では、手順を書きません。書くのはこれだけです。

<span>{isLoggedIn ? "さとうさん" : "ゲスト"}</span>

「ログイン中なら名前、そうでなければゲスト」。 これは手順ではなく、事実の記述です。 いつ書き換えるか、という話が一切出てきません。

状態はひとつだけ

ボタンを押すと 3 か所が同時に変わる

ヘッダーゲスト
ログインすると内容が表示されます

このデモが持っている状態は isLoggedIn ひとつだけです。truefalse を切り替えているだけで、 3 か所を書き換える処理はどこにもありません。

状態isLoggedInヘッダーの名前本文の内容ボタンの文字
状態を 1 つ変えれば、そこから決まる表示はすべてそろって変わる

表示する場所が 4 つに増えても、書き足すのはその 1 か所だけです。 「両方に書き足す」必要がないので、片方だけ直し忘れる事故が起きません。

login-view.tsx

"use client";

import { Button } from "@/components/ui/button";
import { useState } from "react";

export function LoginView() {
  // 画面が持っている状態は、この 1 つだけ
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  return (
    <div className="flex flex-col gap-4">
      <div className="flex items-center justify-between rounded-md border px-3 py-2">
        <span className="text-muted-foreground">ヘッダー</span>
        <span className="font-semibold">
          {isLoggedIn ? "さとうさん" : "ゲスト"}
        </span>
      </div>

      <div className="rounded-md border px-3 py-2">
        {isLoggedIn
          ? "会員向けの内容が表示されています"
          : "ログインすると内容が表示されます"}
      </div>

      <Button size="sm" onClick={() => setIsLoggedIn(!isLoggedIn)}>
        {isLoggedIn ? "ログアウトする" : "ログインする"}
      </Button>
    </div>
  );
}

画面は、状態から決まる

この考え方は、よくこう表現されます。

UI = f(state)

画面は、状態を渡すと結果が返ってくる関数のようなものだ、という意味です。 同じ状態なら、いつ何度描いても同じ画面になります。

だから React では、画面を直接いじりません。状態を変えれば、画面は勝手についてくるからです。

login-view.tsx

"use client";

import { Button } from "@/components/ui/button";
import { useState } from "react";

export function LoginView() {
  // 画面が持っている状態は、この 1 つだけ
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  return (
    <div className="flex flex-col gap-4">
      <div className="flex items-center justify-between rounded-md border px-3 py-2">
        <span className="text-muted-foreground">ヘッダー</span>
        <span className="font-semibold">
          {isLoggedIn ? "さとうさん" : "ゲスト"}
        </span>
      </div>

      <div className="rounded-md border px-3 py-2">
        {isLoggedIn
          ? "会員向けの内容が表示されています"
          : "ログインすると内容が表示されます"}
      </div>

      <Button size="sm" onClick={() => setIsLoggedIn(!isLoggedIn)}>
        {isLoggedIn ? "ログアウトする" : "ログインする"}
      </Button>
    </div>
  );
}

理解できたか確かめる

確認クイズ

宣言的 UI とは、どういう書き方のこと?

確認クイズ

React で「ボタンの文字を変えたい」と思ったとき、まず何を考える?

login-view.tsx

"use client";

import { Button } from "@/components/ui/button";
import { useState } from "react";

export function LoginView() {
  // 画面が持っている状態は、この 1 つだけ
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  return (
    <div className="flex flex-col gap-4">
      <div className="flex items-center justify-between rounded-md border px-3 py-2">
        <span className="text-muted-foreground">ヘッダー</span>
        <span className="font-semibold">
          {isLoggedIn ? "さとうさん" : "ゲスト"}
        </span>
      </div>

      <div className="rounded-md border px-3 py-2">
        {isLoggedIn
          ? "会員向けの内容が表示されています"
          : "ログインすると内容が表示されます"}
      </div>

      <Button size="sm" onClick={() => setIsLoggedIn(!isLoggedIn)}>
        {isLoggedIn ? "ログアウトする" : "ログインする"}
      </Button>
    </div>
  );
}

この章のまとめ

  • 命令的 = 画面をどう変えるかの手順を書く
  • 宣言的 = 状態と画面の対応関係を書く
  • 対応を 1 か所書けば済むので、直し忘れによる食い違いが起きない
  • React では「表示を変えたい」ではなく「状態を変えたい」と考える

login-view.tsx

"use client";

import { Button } from "@/components/ui/button";
import { useState } from "react";

export function LoginView() {
  // 画面が持っている状態は、この 1 つだけ
  const [isLoggedIn, setIsLoggedIn] = useState(false);

  return (
    <div className="flex flex-col gap-4">
      <div className="flex items-center justify-between rounded-md border px-3 py-2">
        <span className="text-muted-foreground">ヘッダー</span>
        <span className="font-semibold">
          {isLoggedIn ? "さとうさん" : "ゲスト"}
        </span>
      </div>

      <div className="rounded-md border px-3 py-2">
        {isLoggedIn
          ? "会員向けの内容が表示されています"
          : "ログインすると内容が表示されます"}
      </div>

      <Button size="sm" onClick={() => setIsLoggedIn(!isLoggedIn)}>
        {isLoggedIn ? "ログアウトする" : "ログインする"}
      </Button>
    </div>
  );
}