条件によって出し分ける

Part 3 · 画面に動きをつける

条件によって出し分ける

&& を使うと画面に 0 が出てしまう問題

ログインしていたら名前を出す。0 件なら何も出さない。読み込み中はぐるぐるを出す。 画面には必ず「場合によって変わる部分」があります。

JSX の中では if が書けません(Part 1 でやったとおり、 波括弧の中には式しか置けないため)。 その代わりに使う書き方が 3 つあります。

そしてそのうちのひとつには、有名な落とし穴があります

2 つのうちどちらかなら、三項演算子

「A か B のどちらかを出す」なら、三項演算子が素直です。

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

条件 ? 真のとき : 偽のとき という形です。 これは式なので、JSX の波括弧の中に置けます。

ただし、入れ子にすると一気に読めなくなります。

// 読めたものではない
{isLoading ? <Spinner /> : isEmpty ? <Empty /> : <List />}

2 段以上になったら、次に出てくる早期 return に切り替えます。

patterns.tsx

"use client";

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

type Status = "loading" | "empty" | "ready";

// 出し分けが増えてきたら、早めに return してしまうほうが読みやすい
function Result({ status }: { status: Status }) {
  if (status === "loading") {
    return <p className="text-muted-foreground">読み込み中…</p>;
  }

  if (status === "empty") {
    return <p className="text-muted-foreground">データがありません</p>;
  }

  return <p className="font-semibold">3 件のデータがあります</p>;
}

export function Patterns() {
  const [status, setStatus] = useState<Status>("loading");

  return (
    <div className="flex flex-col gap-4">
      <div className="rounded-md border p-3">
        <Result status={status} />
      </div>

      <div className="flex flex-wrap gap-2">
        {(["loading", "empty", "ready"] as const).map((value) => (
          <Button
            key={value}
            size="sm"
            variant={status === value ? "default" : "outline"}
            onClick={() => setStatus(value)}
          >
            {value}
          </Button>
        ))}
      </div>
    </div>
  );
}

「あるときだけ出す」の落とし穴

「条件を満たすときだけ出す。そうでなければ何も出さない」場合、&& を使います。

{items.length && <p>{items.length} 件あります</p>}

一見よさそうですが、これは壊れています。 下のデモで「空にする」を押してみてください。

件数がないときの表示うまくいかない例

空にすると 0 が残る

1 件あります

何も出ないはずが、0 という文字が表示されました

理由は Part 0-5 でやったとおりです。&& は true / false を返すのではなく、左が偽ならその左の値をそのまま返します。 件数が 0 のとき、この式は 0 になります。

そして React は、数値を画面に表示しますfalse なら消えてくれるのに、0 は表示対象なので残ってしまうのです。

broken-condition.tsx

"use client";

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

export function BrokenCondition() {
  const [items, setItems] = useState<string[]>(["りんご"]);

  return (
    <div className="flex flex-col gap-4">
      <div className="rounded-md border p-3">
        {/* 0 件のとき、この式は 0 になる。そして React は 0 を表示する */}
        {items.length && <p>{items.length} 件あります</p>}
      </div>

      <div className="flex gap-2">
        <Button size="sm" onClick={() => setItems([...items, "りんご"])}>
          増やす
        </Button>
        <Button size="sm" variant="outline" onClick={() => setItems([])}>
          空にする
        </Button>
      </div>
    </div>
  );
}

直し方

&& の左を、必ず true / false にしてから渡します。

// ✕ 0 が画面に出る
{items.length && <p>…</p>}

// ○ 比較して true / false にする
{items.length > 0 && <p>…</p>}
比較してから渡す直した例

空にしても何も出ない

1 件あります

fixed-condition.tsx

"use client";

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

export function FixedCondition() {
  const [items, setItems] = useState<string[]>(["りんご"]);

  return (
    <div className="flex flex-col gap-4">
      <div className="rounded-md border p-3">
        {/* 比較して true / false にしてから && に渡す。
            false は画面に出ないので、0 件のときは何も表示されない */}
        {items.length > 0 && <p>{items.length} 件あります</p>}
      </div>

      <div className="flex gap-2">
        <Button size="sm" onClick={() => setItems([...items, "りんご"])}>
          増やす
        </Button>
        <Button size="sm" variant="outline" onClick={() => setItems([])}>
          空にする
        </Button>
      </div>
    </div>
  );
}

場合が増えたら、早めに返す

出し分けが 3 つ以上になったら、三項演算子を重ねるより先に返してしまうほうが読みやすくなります。

function Result({ status }: { status: Status }) {
  if (status === "loading") {
    return <p>読み込み中…</p>;
  }

  if (status === "empty") {
    return <p>データがありません</p>;
  }

  return <p>3 件のデータがあります</p>;
}

コンポーネントはただの関数なので、JSX の外側では if が自由に使えます。 書けないのは JSX の波括弧の中だけです。

状態ごとに表示を変える

ボタンで状態を切り替えられる

読み込み中…

この形の良いところは、それぞれの場合が独立して読めることです。 「読み込み中はこれ」で話が終わり、次の行に進めます。

何も出したくないときは null

function Banner({ isVisible }: { isVisible: boolean }) {
  if (!isVisible) return null;

  return <div>お知らせ</div>;
}

null を返すと、何も表示されません。 「条件を満たさないときは、そもそも何も描かない」を はっきり書けます。

patterns.tsx

"use client";

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

type Status = "loading" | "empty" | "ready";

// 出し分けが増えてきたら、早めに return してしまうほうが読みやすい
function Result({ status }: { status: Status }) {
  if (status === "loading") {
    return <p className="text-muted-foreground">読み込み中…</p>;
  }

  if (status === "empty") {
    return <p className="text-muted-foreground">データがありません</p>;
  }

  return <p className="font-semibold">3 件のデータがあります</p>;
}

export function Patterns() {
  const [status, setStatus] = useState<Status>("loading");

  return (
    <div className="flex flex-col gap-4">
      <div className="rounded-md border p-3">
        <Result status={status} />
      </div>

      <div className="flex flex-wrap gap-2">
        {(["loading", "empty", "ready"] as const).map((value) => (
          <Button
            key={value}
            size="sm"
            variant={status === value ? "default" : "outline"}
            onClick={() => setStatus(value)}
          >
            {value}
          </Button>
        ))}
      </div>
    </div>
  );
}

理解できたか確かめる

確認クイズ

{items.length && <p>…</p>} で、items が空のとき何が起きる?

確認クイズ

JSX の中で if が書けないのに、コンポーネントの中では if が書けるのはなぜ?

broken-condition.tsx

"use client";

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

export function BrokenCondition() {
  const [items, setItems] = useState<string[]>(["りんご"]);

  return (
    <div className="flex flex-col gap-4">
      <div className="rounded-md border p-3">
        {/* 0 件のとき、この式は 0 になる。そして React は 0 を表示する */}
        {items.length && <p>{items.length} 件あります</p>}
      </div>

      <div className="flex gap-2">
        <Button size="sm" onClick={() => setItems([...items, "りんご"])}>
          増やす
        </Button>
        <Button size="sm" variant="outline" onClick={() => setItems([])}>
          空にする
        </Button>
      </div>
    </div>
  );
}

この章のまとめ

  • 2 択なら三項演算子。入れ子にしない
  • あるときだけ出すなら &&。ただし左は必ず true / false にする
  • 0 は falsy だが画面に表示されるlength > 0 のように比較を挟む
  • 場合が 3 つ以上なら早期 return。JSX の外なら if が使える
  • 何も出したくないときは null を返す

fixed-condition.tsx

"use client";

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

export function FixedCondition() {
  const [items, setItems] = useState<string[]>(["りんご"]);

  return (
    <div className="flex flex-col gap-4">
      <div className="rounded-md border p-3">
        {/* 比較して true / false にしてから && に渡す。
            false は画面に出ないので、0 件のときは何も表示されない */}
        {items.length > 0 && <p>{items.length} 件あります</p>}
      </div>

      <div className="flex gap-2">
        <Button size="sm" onClick={() => setItems([...items, "りんご"])}>
          増やす
        </Button>
        <Button size="sm" variant="outline" onClick={() => setItems([])}>
          空にする
        </Button>
      </div>
    </div>
  );
}