Part 1 · React の考え方
10 / 64 章
宣言的 UI
画面を書き換えるのをやめて、状態を書き換える
React の説明でよく出てくる「宣言的 UI」という言葉。 日本語として硬いので身構えてしまいますが、 言っていることはひとつだけです。
画面をどう作り変えるかではなく、画面がどうあるべきかを書く。
この違いが分かると、React のコードが「なぜあの形をしているのか」に納得がいきます。
手順を書くやり方(命令的)
ログイン状態によって、画面の 3 か所を変えたいとします。 ヘッダーの名前、本文、ボタンの文字です。
手順として書くと、こうなります。 (header.textContent = "…" は「ヘッダーの文字を、これに書き換えろ」という命令です。 前の章で見た素の JavaScript と同じ書き方です)
function login() {
header.textContent = "さとうさん";
body.textContent = "会員向けの内容が表示されています";
button.textContent = "ログアウトする";
}
function logout() {
header.textContent = "ゲスト";
body.textContent = "ログインすると内容が表示されます";
button.textContent = "ログインする";
}間違ってはいません。ただ、同じことを 2 回、逆向きに書いているのが気になります。
そして表示する場所が 1 つ増えるたびに、login と logout の両方に書き足す必要があります。 片方だけ直してしまうと、 「ログアウトしたのにヘッダーだけ名前が残っている」という状態が生まれます。
login-view.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";
export function LoginView() {
// このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
useTrackDemoRender();
// 画面が持っている状態は、この 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 ひとつだけです。true と false を切り替えているだけで、 3 か所を書き換える処理はどこにもありません。
表示する場所が 4 つに増えても、書き足すのはその 1 か所だけです。 「両方に書き足す」必要がないので、片方だけ直し忘れる事故が起きません。
login-view.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";
export function LoginView() {
// このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
useTrackDemoRender();
// 画面が持っている状態は、この 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)記号が並んでいますが、身構えなくて大丈夫です。f は関数のこと。UI は画面、state は状態です。 つまり「状態を入れると、画面が出てくる箱」と書いてあるだけです。
画面は、状態を渡すと結果が返ってくる関数のようなものだ、という意味です。 同じ状態なら、いつ何度描いても同じ画面になります。
だから React では、画面を直接いじりません。状態を変えれば、画面は勝手についてくるからです。
login-view.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";
export function LoginView() {
// このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
useTrackDemoRender();
// 画面が持っている状態は、この 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>
);
}状態が 2 つになると、差が開く
状態が 1 つのうちは、手順で書いてもそれほど困りません。2 つになると、はっきり差が出ます。
「ログイン中かどうか」に加えて「未読があるかどうか」が増えたとします。 手順で書くなら、組み合わせの数だけ書くことになります。
// 2 × 2 = 4 通り。それぞれで 3 か所を書き換える
function update() {
if (isLoggedIn && hasUnread) { /* 3 か所 */ }
else if (isLoggedIn) { /* 3 か所 */ }
else if (hasUnread) { /* 3 か所 */ }
else { /* 3 か所 */ }
}状態が 3 つになれば 8 通り、4 つなら 16 通り。倍々に増えていきます。
宣言的に書けば、増えるのは状態のほうだけです。
2 つのチェックを自由に組み合わせてみる
ゲスト
ログインすると内容が表示されます
4 通りぶんの手順は、どこにも書いていません。書いてあるのは「この状態なら、こう出す」という対応関係だけです。組み合わせは React が計算します。
two-states.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { useState } from "react";
export function TwoStates() {
useTrackDemoRender();
// 状態は 2 つだけ
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [hasUnread, setHasUnread] = useState(false);
return (
<div className="flex flex-col gap-3">
<div className="flex gap-4 text-sm">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={isLoggedIn}
onChange={(event) => setIsLoggedIn(event.target.checked)}
/>
ログイン中
</label>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={hasUnread}
onChange={(event) => setHasUnread(event.target.checked)}
/>
未読あり
</label>
</div>
{/* 画面の 3 か所が、上の 2 つから自動的に決まる */}
<div className="flex flex-col gap-2 rounded-md border p-3 text-sm">
<p className="font-semibold">
{isLoggedIn ? "さとうさん" : "ゲスト"}
{isLoggedIn && hasUnread && (
<span className="ml-2 rounded-full bg-red-500 px-2 py-0.5 text-xs text-white">
未読
</span>
)}
</p>
<p className="text-muted-foreground">
{isLoggedIn
? hasUnread
? "新しいお知らせがあります"
: "新しいお知らせはありません"
: "ログインすると内容が表示されます"}
</p>
{/*
ここは表示だけの飾り。直前の「うまくいかない例」には
見た目がそっくりで押すと動くボタンがあるので、
押せないことが見て分かるようにしておく。
*/}
<button
type="button"
disabled
className="self-start rounded-md border px-3 py-1.5 opacity-50"
>
{isLoggedIn ? "ログアウトする" : "ログインする"}
</button>
</div>
</div>
);
}理解できたか確かめる
確認クイズ
宣言的 UI とは、どういう書き方のこと?
確認クイズ
React で「ボタンの文字を変えたい」と思ったとき、まず何を考える?
login-view.tsx
"use client";
import { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";
export function LoginView() {
// このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
useTrackDemoRender();
// 画面が持っている状態は、この 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 { useTrackDemoRender } from "@/components/lesson/demo-card";
import { Button } from "@/components/ui/button";
import { useState } from "react";
export function LoginView() {
// このカードが描き直された回数を数えるための 1 行(教材の仕掛け)
useTrackDemoRender();
// 画面が持っている状態は、この 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>
);
}