【UIデザイナーのためのWeb技術入門】気持ちいいUIの縁の下の力持ち、非同期通信


こんにちは。UIデザイナーの梁川です。React入門シリーズの第2回です。
前回はHabit Trackerのヘッダー部分として、アプリ名と日付範囲、Prev/Nextボタンを作りました。見た目としては完成していますが、よく見るとコードには重複している箇所があります。
<button className="bg-zinc-800 hover:bg-zinc-200 text-white hover:text-black transition-colors duration-200 ease-out rounded-full px-6 py-3">
Prev
</button>
<button className="bg-zinc-800 hover:bg-zinc-200 text-white hover:text-black transition-colors duration-200 ease-out rounded-full px-6 py-3">
Next
</button>これら2つのボタンは、”Prev” と “Next” のテキスト以外は全て共通していることがわかると思います。さらに今後、同じ形のボタンを他の箇所でも使用します。今回はこれを1つの部品にまとめていきます。
Button という専用のコンポーネントを作り、Prev/Nextボタンをコンポーネントで置き換えていきます。あわせて、前回 App.jsx の中に書いていた Header も専用ファイルに引っ越しさせます。
src/
├── App.jsx
├── main.jsx
├── index.css
└── components/
├── Button.jsx ← 新規
└── Header.jsx ← App.jsx から移動今回作成する Button はいろいろな場所から呼ばれる部品になります。部品は部品でまとめておきたいので、src/components/ というフォルダを作り、その中に Button.jsx を用意します。
// src/components/Button.jsx
export function Button({ children }) {
return (
<button className="bg-zinc-800 hover:bg-zinc-200 text-white hover:text-black transition-colors duration-200 ease-out rounded-full px-6 py-3 disabled:opacity-50 disabled:cursor-not-allowed">
{children}
</button>
);
}前回のボタンの className をそのまま持ってきて、テキストだった部分を {children} に置き換えています。この children については、次の節で詳しく見ていきます。
ここで押さえておきたいポイントは3つです。
button ではなく Button と大文字で始めてください。JSXでは、小文字始まりのタグは「HTMLの標準タグ」、大文字始まりのタグは「自作のコンポーネント」として区別されます。
export で他のファイルから使えるようにする関数の頭に付いている export は、Reactの機能ではなくJavaScript標準の仕組み(ESモジュール)です。「この関数を他のファイルから import できるようにする」という宣言で、これがないと Button は Button.jsx の中でしか使えません。
クラスの末尾に disabled:opacity-50 disabled:cursor-not-allowed の2つを足しました。disabled: は前回出てきた hover: と同じ仲間で、ボタンに disabled 属性が付いているときだけ効くクラスです。
| Tailwind | CSS |
|---|---|
disabled:opacity-50 | :disabled { opacity: 0.5; } |
disabled:cursor-not-allowed | :disabled { cursor: not-allowed; } |
試しに Button.jsx の <button> に disabled 属性を直接書いてみると、ボタンが半透明になり、カーソルを乗せると禁止マークに変わります。

今のHabit Trackerに「押せないボタン」はまだありませんが、今後「未来の日付は押せない」仕様を実装します。Button の中に、通常・ホバー・押せない、という状態ごとの見た目をまとめて作っておくと便利です。
App.jsx に戻って、Button を読み込み、2つの <button> を置き換えていきます。
// App.jsx
import { Button } from './components/Button';
export default function App() {
return (
<div className="max-w-6xl mx-auto p-4 flex flex-col gap-4">
<Header />
</div>
);
}
function Header() {
return (
<div>
<header className="flex items-center justify-between p-4">
<div className="flex flex-col gap-1">
<h1 className="text-3xl">Habit Tracker</h1>
<p className="text-zinc-400 text-sm">Your personal habit tracking app</p>
</div>
<div className="flex flex-col items-end gap-4">
<span className="text-zinc-400 text-sm pr-4">Apr/1 - Apr/7</span>
<div className="flex items-center gap-2">
<Button>Prev</Button>
<Button>Next</Button>
</div>
</div>
</header>
</div>
);
}呼び出し側は <Button>Prev</Button> だけになりました。ブラウザを確認すると、見た目は前回と何も変わっていないはずです。
import の書き方import { Button } from './components/Button';{ Button } と波括弧で囲んでいるのは、Button.jsx 側が export function Button という名前付きエクスポートだからです。一方、App.jsx の export default function App はデフォルトエクスポートで、こちらは main.jsx で import App from './App.jsx' と波括弧なしで読み込まれています。
さて、Button.jsx に戻って { children } の部分を見ていきます。
Button はただのJavaScript関数です。関数なら引数を渡せます。Reactではこの引数のことを Props(プロパティ)と呼びます。
Figmaで例えると、コンポーネントに付ける「プロパティ」がこれに当たります。Button コンポーネントを1つ作っておいて、「Label」プロパティでテキストだけを差し替えるイメージです。同じ見た目の部品を、中身だけ変えて使い回すための仕組みがPropsです。
Reactのルールとして、Propsは必ず1つのオブジェクトとして関数に渡されます。なので本来はこう書きます。
export function Button(props) {
return (
<button className="...">
{props.children}
</button>
);
}props はオブジェクトなので、props.children のように . で中身を取り出します。これをオブジェクトの分割代入で短く書いたのが、最初に出てきた { children } の形です。
export function Button({ children }) {
return (
<button className="...">
{children}
</button>
);
}children は「タグの間に挟んだもの」children には開始タグと終了タグの間に挟んだ内容(この場合は Prev という文字列)が、自動的に children という名前のPropsとして渡ってきています。
<Button>Prev</Button>
// ^^^^ これが children に入るchildren という名前は自分で決めたものではなく、Reactが予約している特別な名前です。HTMLの <label>テキスト</label> のようにタグの中に何かを挟むという書き方そのものが、Reactでは「中身を children として渡す」という意味になります。
そして Button 側では {children} と波括弧で囲んで <button> の中に置いています。受け取った Prev という文字列が、そのまま <button> の中身として描画されます。
ちなみに、<Header /> のように /> で閉じているタグは「挟むものがない」という書き方です。<Header></Header> と書いても同じ意味ですが、中身がないときは短く書くのが一般的です。
Button を部品化したついでに、App.jsx の中に書いていた Header も専用ファイルに移しましょう。今後フォームやリストのコンポーネントが増えていくと、全部を App.jsx に書くと見通しが悪くなるためです。
src/components/Header.jsx を作り、Header 関数をそのまま移して export を付けます。
// src/components/Header.jsx
import { Button } from './Button';
export function Header() {
return (
<div>
<header className="flex items-center justify-between p-4">
<div className="flex flex-col gap-1">
<h1 className="text-3xl">Habit Tracker</h1>
<p className="text-zinc-400 text-sm">Your personal habit tracking app</p>
</div>
<div className="flex flex-col items-end gap-4">
<span className="text-zinc-400 text-sm pr-4">Apr/1 - Apr/7</span>
<div className="flex items-center gap-2">
<Button>Prev</Button>
<Button>Next</Button>
</div>
</div>
</header>
</div>
);
}Button の import も一緒に移動しますが、パスが './Button' に変わっている点に注意してください。Header.jsx と Button.jsx は同じ components/ フォルダにあるので、./components/ は不要になります。
// App.jsx
import { Header } from './components/Header';
export default function App() {
return (
<div className="max-w-6xl mx-auto p-4 flex flex-col gap-4">
<Header />
</div>
);
}第2回は以上です。おつかれさまでした。
今回出てきたことをおさらいしておきましょう。
components/ フォルダにまとめ、export / import でやり取りします。hover: や disabled: のような状態ごとのスタイルは、使う側ではなく部品側で設定しておきます。{ children } のように分割代入で受け取ります。children — JSXタグに挟んだ内容を自動で受け取ってくれる、Reactの特別なProps。見た目は前回とまったく同じですが、コードがすっきりしました。地味な作業に見えますが、今後この考え方が活きてきます。次回は習慣を追加するための入力フォームを作っていきますが、早速今回作った Button を使用します。
ではまた。