本文までスキップする

【初心者向け】ReactでモダンなUIを実装しよう【Part2】

アバター画像
Takuma Yanagawa
/ 4 views

こんにちは。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コンポーネントを作る

今回作成する 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つです。

1. コンポーネント名は大文字で始める

button ではなく Button と大文字で始めてください。JSXでは、小文字始まりのタグは「HTMLの標準タグ」、大文字始まりのタグは「自作のコンポーネント」として区別されます。

2. export で他のファイルから使えるようにする

関数の頭に付いている export は、Reactの機能ではなくJavaScript標準の仕組み(ESモジュール)です。「この関数を他のファイルから import できるようにする」という宣言で、これがないと Button は Button.jsx の中でしか使えません。

3. 押せない状態の見た目も作っておく

クラスの末尾に disabled:opacity-50 disabled:cursor-not-allowed の2つを足しました。disabled: は前回出てきた hover: と同じ仲間で、ボタンに disabled 属性が付いているときだけ効くクラスです。

TailwindCSS
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> と書いても同じ意味ですが、中身がないときは短く書くのが一般的です。

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: のような状態ごとのスタイルは、使う側ではなく部品側で設定しておきます。
  • Props — コンポーネントに渡す引数。必ず1つのオブジェクトとして渡されるので、{ children } のように分割代入で受け取ります。
  • children — JSXタグに挟んだ内容を自動で受け取ってくれる、Reactの特別なProps。

見た目は前回とまったく同じですが、コードがすっきりしました。地味な作業に見えますが、今後この考え方が活きてきます。次回は習慣を追加するための入力フォームを作っていきますが、早速今回作った Button を使用します。
ではまた。

参考