🐶

Reactチュートリアル1:犬画像ギャラリーを作ろう

2020/10/24に公開

本資料について

本資料は日本大学文理学部情報科学科の開講科目「Web プログラミング」の教材として作成されました。本資料は下記のライセンスの範囲内で、当授業以外でも自由にご利用いただけます。

対象読者

本資料は、以下の教材を学習済み、もしくはそれと同等以上の知識を持っていることを前提としています。

本資料で学ぶこと

本資料では以下の内容を学びます。

  • React の基本
    • 開発の始め方
    • JSX
    • コンポーネントと props
    • 条件分岐と繰り返し
    • useEffect による副作用の扱い
    • useState による状態管理
    • フォームとイベントハンドリング
  • Netlify による Web アプリの公開

ライセンス

license この作品はクリエイティブ・コモンズ 表示 4.0 国際 ライセンスの下に提供されています。

React とは

React は JavaScript でユーザーインタフェースを構築するためのライブラリです。実用的な Web アプリケーションでは、ユーザーの操作やサーバーとの通信結果に応じてユーザーインタフェース、すなわち HTML を動的に組み換えていかなければいけません。

ユーザーの操作やサーバーとの通信はアプリケーションの状態を更新します。アプリケーションの状態とは、例えば以下のようなものがあります。

  • SNS サイトのタイムライン
  • EC サイトにおけるショッピングカートの内容
  • ボードゲームの盤面

React はアプリケーションの状態からユーザーインタフェースを組み立てる方法と、アプリケーションの状態を更新するための仕組みを提供してくれます。React を利用することで、ユーザーインタラクションを含んだ複雑なユーザーインタフェースを構築することが簡単になります。

セットアップ

はじめに package.json を作成します。Node.js を使ったアプリケーション開発では、ひとまとまりのプログラムを パッケージ という単位で管理します。React もパッケージの 1 つです。package.json は、パッケージの情報を記述するためのファイルです。

作業用のディレクトリを作っておきましょう。説明は react-tutorial というディレクトリの中で行います。

以下のコマンドで package.json が作られます。$ はコマンドの始まりを表す記号です。実際にタイプするのは npm init -y の部分のみです。

$ npm init -y

作られたファイルの中身は以下のようになっています。
name プロパティがパッケージの名前を表していて、ディレクトリ名と同じ react-tutorial となっています。

package.json
{
  "name": "react-tutorial",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

パッケージには依存関係があります。これから作る react-tutorial というパッケージは React を使うので、reactreact-dom というパッケージに依存することになります。また、React アプリケーションのビルドや、ローカル開発サーバーの起動に vite というパッケージを使います。react-tutorialvite にも依存することになります。

パッケージの依存関係にはいくつかの種類があります。その種類の主要なものとして、アプリケーションが使用するライブラリの依存関係と、アプリケーションを開発するために使うツールの依存関係があります。reactreact-dom は前者に、vite は後者に該当します。

依存するパッケージをインストールし管理するために npm install コマンドを使用します。

まずは、アプリケーションが使用するライブラリである reactreact-dom をインストールしてみましょう。以下のコマンドを実行してください。(npm install の代わりに、短く npm i とすることもできます。)

$ npm install react react-dom

続けて、アプリケーションを開発するためのツールである vite とそのプラグインをインストールしてみましょう。以下のコマンドを実行してください。(--save-dev の代わりに、短く -D とすることもできます。)

$ npm install --save-dev vite @vitejs/plugin-react

インストールした依存パッケージは package.json に記録されます。3 つのパッケージをインストールした後の package.json は以下のようになっているでしょう。--save-dev を付けてインストールしたパッケージは devDependencies の中に、付けずにインストールしたパッケージは dependencies に含まれます。

package.json
{
  "name": "react-tutorial",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^18.1.0",
    "react-dom": "^18.1.0"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "^1.3.2",
    "vite": "^2.9.9"
  }
}

準備の仕上げとして、package.json を編集しましょう。以下の - の付いた赤色の行を削除して、+ のついた緑色の行を追記してください。行頭の-+ は入力しないようにしてください。このような表記を diff と呼びます。

package.json
 {
   "name": "react-tutorial",
   "version": "1.0.0",
   "description": "",
   "main": "index.js",
   "scripts": {
-    "test": "echo \"Error: no test specified\" && exit 1"
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
   },
   "keywords": [],
   "author": "",
   "license": "ISC",
   "dependencies": {
     "react": "^18.1.0",
     "react-dom": "^18.1.0"
   },
   "devDependencies": {
     "@vitejs/plugin-react": "^1.3.2",
     "vite": "^2.9.9"
   }
 }

念のため、編集後の package.json の中身も示しておきます。diff によるソースコードの差分表記に読み慣れておきましょう。

package.json
{
  "name": "react-tutorial",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "react": "^18.1.0",
    "react-dom": "^18.1.0"
  },
  "devDependencies": {
    "@vitejs/plugin-react": "^1.3.2",
    "vite": "^2.9.9"
  }
}

編集内容の説明をしておきます。scripts プロパティにはコマンドのショートカットを登録しておくことができます。npm run build とコマンドを実行すると build に対応する vite build が実行されます。開発時に頻繁に使用するコマンドは scripts の中に書いておくと良いでしょう。

Hello, World!

準備ができたらアプリケーションのコードを書いていきましょう。まず、src ディレクトリを作成しましょう。そして、プロジェクトのルートディレクトリ( package.json があるディレクトリ)の中に vite.config.jsindex.html を、src の中に App.jsxmain.jsx をそれぞれ以下の内容で作成します。

vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
});
index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>React Tutorial</title>
  </head>
  <body>
    <div id="content"></div>
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>
src/App.jsx
function App() {
  return (
    <div>
      <h1>Hello, World!</h1>
    </div>
  );
}

export default App;
src/main.jsx
import { createRoot } from "react-dom/client";
import App from "./App";

createRoot(document.querySelector("#content")).render(<App />);

ここまででディレクトリの中身は以下のようになっているでしょう。

├── index.html
├── node_modules
│   // 省略
├── package-lock.json
├── package.json
├── src
│   ├── App.jsx
│   └── main.jsx
└── vite.config.js

さあ、React アプリケーションを実行してみましょう。ローカル開発サーバーを起動するには以下のコマンドを実行します。

$ npm run dev

http://localhost:5173/ にアクセスし、以下のように表示されていたら成功です。React の世界へようこそ!うまくいかなかったら、これまでの手順をよく見直してやり直してみてください。

Hello, World!

完成イメージ

これから React を使って Web アプリケーションを開発していきます。まずは、何を作るかのイメージを固めておきましょう。本来は 1 から自由に発送するところですが、本稿ではあらかじめ用意した簡単なアプリケーションを手順に沿って作ってもらいます。

Dog API はたくさんの犬の画像を提供している Web API です。Dog API を利用して、犬画像ギャラリーを作ります。最初に完成図を示しておきます。

犬画像ギャラリーの完成イメージ

犬画像ギャラリーにアクセスすると、何枚かの犬の画像が表示されます。ページの上部にはフォームが付いており、表示する画像の犬種を選ぶことができます。

それでは始めていきましょう!

Web アプリをマークアップする

React では、JSX という拡張構文で、 JavaScript の中に HTML のマークアップを記述します。本稿では、JSX によって記述されたマークアップを JSX 式 と呼ぶことにします。以下のプログラムの 3〜5 行目が JSX 式にあたります。

function App() {
  return (
    <div>
      <h1>Hello, World!</h1>
    </div>
  );
}

ここで一旦、先ほどローカル開発サーバーを起動して Web ページにアクセスしたときに何が起きたのか説明しましょう。

src/App.jsx では、App という関数を定義しています。React では、JSX を返すような関数を特別に コンポーネント と呼びます。コンポーネントは、アプリケーションの構成部品であり、状態管理の単位となります。コンポーネントは、アプリケーションの状態から JSX 式を組み立てて、その結果を HTML としてレンダリングします。

index.htmlbody要素の中には、contentid に持った div 要素(#content と呼びましょう)が含まれています。src/main.jsxsrc/App.jsx からインポートした App コンポーネントを、#content にマウントすることで、App コンポーネントが組み立てた JSX 式によってレンダリングされた HTML を Web ブラウザ上に表示します。

さて、犬画像ギャラリーの開発に戻ります。src/App.jsx を以下のように書き換えてみましょう。

src/App.jsx
function App() {
  return (
    <div>
      <header>
        <h1>Cute Dog Images</h1>
      </header>
      <main>
        <section>
          <figure>
            <img
              src="https://images.dog.ceo/breeds/shiba/shiba-8.jpg"
              alt="cute dog"
            />
          </figure>
        </section>
      </main>
      <footer>
        <p>Dog images are retrieved from Dog API</p>
        <p>
          <a href="https://dog.ceo/dog-api/about">Donate to Dog API</a>
        </p>
      </footer>
    </div>
  );
}

export default App;

ローカル開発サーバーを起動して Web ブラウザでページを開いていれば、src/App.jsx を編集して保存すれば自動的に Web ブラウザのページがリロードされて以下のように表示されるでしょう。ローカル開発サーバーを停止していた場合は、npm start で再度起動しましょう。

最初のマークアップ

さて、お気付きのように JSX は ほとんど 普通の HTML と同じように書くことができます。知っている HTML の要素を書いてみると良いでしょう。

ただし、JSX は JavaScript の一部であり、文法に従って正しく書く必要があります。HTML では文法を間違えても無視されたり、意図しない表示になるだけですが、JSX の場合は文法エラーが起きてページが表示されません。ローカル開発サーバーで開発している間はエラーページが表示されるでしょう。

HTML では終了タグの省略が許される場合がありますが、JSX では必ず開始タグと終了タグのセットか空要素のどちらでなければいけません。それぞれの例を以下に示します。

function App() {
  return (
    <div>
      <p>開始タグと終了タグ</p>
      <textarea value="空要素" />
    </div>
  );
}

以下の p 要素のように開始タグだけで終了タグがない場合は文法エラーとなります。

function App() {
  return (
    <div>
      <p>開始タグのみ
    </div>
  );
}

Web ブラウザ上では以下のように表示されるでしょう。

エラー画面

エラーが発生したときは、落ち着いて文法的に正しいマークアップになっているか見直しましょう。

CSS フレームワークを使う

さて、犬画像ギャラリーの開発を進めましょう。今の見た目は少し質素すぎるので、CSS で Web ページの見た目を整えていきましょう。

アプリケーション全体で統一感のあるデザインを 1 から CSS で作るのはなかなか難しいものです。そんなときには、よくデザインされた UI パーツ集である CSS フレームワークを利用できます。本稿では、CSS フレームワークの一種である Bulma を使用します。

npm を使って Bulma をインストールしましょう。ローカル開発サーバーが起動中の場合は、Ctrl+C で一旦停止してからコマンドを入力しましょう。以下のコマンドを実行してください。

$ npm install bulma

Bulma の CSS を読み込むために、src/main.jsx の先頭を以下のように書き加えましょう。

src/main.jsx
+import "bulma/css/bulma.css";
+
 import { createRoot } from "react-dom/client";
 import App from "./App";

 createRoot(document.querySelector("#content")).render(<App />);

Bulma では、要素に class を付与していくことで、Bulma が用意したデザインが付与されます。class 属性は、JSX では className と書く必要があるので注意しましょう。

src/App.jsx を以下のように書き直します。

src/App.jsx
function App() {
  return (
    <div>
      <header className="hero is-dark is-bold">
        <div className="hero-body">
          <div className="container">
            <h1 className="title">Cute Dog Images</h1>
          </div>
        </div>
      </header>
      <main>
        <section className="section">
          <div className="container">
            <div className="columns is-vcentered is-multiline">
              <div className="column is-3">
                <div className="card">
                  <div className="card-image">
                    <figure className="image">
                      <img
                        src="https://images.dog.ceo/breeds/shiba/shiba-8.jpg"
                        alt="cute dog"
                      />
                    </figure>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </section>
      </main>
      <footer className="footer">
        <div className="content has-text-centered">
          <p>Dog images are retrieved from Dog API</p>
          <p>
            <a href="https://dog.ceo/dog-api/about">Donate to Dog API</a>
          </p>
        </div>
      </footer>
    </div>
  );
}

export default App;

Web ページの表示結果を見てみましょう。少しはカッコよくなりましたか?

Bulmaによるデザイン

こんなんじゃまだ満足しないという人はオリジナルのデザインを考えてみましょう。Bulma のドキュメント を眺めてみて、面白そうな表現があったら試してみると良いでしょう。

コンポーネントの分割

さて、見た目を整えるためにApp コンポーネントに要素をたくさん書いてきましたが、Appコンポーネントの中身が随分長くなってしまいました。こういうときはコンポーネントの分割をしましょう。

HTML は階層構造を持ちますが、あるコンポーネントの JSX 式の中に別のコンポーネントを書くことで、コンポーネントも階層化することができます。コンポーネントを使う側のコンポーネントを 親コンポーネント、別のコンポーネントから使われる側のコンポーネントを 子コンポーネント と呼びます。

App コンポーネントの中身を、以下のようにHeaderImageGalleryMainFooterの 5 つのコンポーネントに分割してみましょう。

src/App.jsx
function Header() {
  return (
    <header className="hero is-dark is-bold">
      <div className="hero-body">
        <div className="container">
          <h1 className="title">Cute Dog Images</h1>
        </div>
      </div>
    </header>
  );
}

function Image() {
  return (
    <div className="card">
      <div className="card-image">
        <figure className="image">
          <img
            src="https://images.dog.ceo/breeds/shiba/shiba-8.jpg"
            alt="cute dog!"
          />
        </figure>
      </div>
    </div>
  );
}

function Gallery() {
  return (
    <div className="columns is-vcentered is-multiline">
      <div className="column is-3">
        <Image />
      </div>
    </div>
  );
}

function Main() {
  return (
    <main>
      <section className="section">
        <div className="container">
          <Gallery />
        </div>
      </section>
    </main>
  );
}

function Footer() {
  return (
    <footer className="footer">
      <div className="content has-text-centered">
        <p>Dog images are retrieved from Dog API</p>
        <p>
          <a href="https://dog.ceo/dog-api/about">Donate to Dog API</a>
        </p>
      </div>
    </footer>
  );
}

function App() {
  return (
    <div>
      <Header />
      <Main />
      <Footer />
    </div>
  );
}

export default App;

それぞれのコンポーネントに含まれる要素数が少なくなったことと、コンポーネントに名前を付けたことで、コンポーネントの役割がわかりやすくなりました。コンポーネントの階層構造は以下のようになっています。

App
├── Header
├── Main
│   └── Gallery
│       └── Image
└── Footer

意味のある要素のまとまりをコンポーネント化することで、アプリケーション全体の構造をわかりやすく表現することができます。

式の埋め込み

ここまでで、アプリケーションの見た目とソースコードを綺麗にできました。犬画像ギャラリーの目的に立ち返って、Dog API から画像を取得して表示することを考えてみましょう。これにはいくつかのステップが必要ですが、最終的には Dog API から取得したデータに含まれる画像の URL を、 Image コンポーネント中の img 要素の src 属性に渡す必要があるでしょう。

JavaScript の式を JSX に埋め込むためには {式}のように書きます。 img 要素の src 属性に書いていた画像の URL を一旦変数に格納し、変数の値を JSX に渡してみましょう。Image コンポーネントを以下のように書き換えます。

src/App.jsx
 function Image() {
+  const url = "https://images.dog.ceo/breeds/shiba/shiba-8.jpg";
   return (
     <div className="card">
       <div className="card-image">
         <figure className="image">
+          <img src={url} alt="cute dog!" />
-          <img
-            src="https://images.dog.ceo/breeds/shiba/shiba-8.jpg"
-            alt="cute dog!"
-          />
         </figure>
       </div>
     </div>
   );
 }

Image コンポーネントは、React のコンポーネントであると同時にただの JavaScript の関数です。そのため、関数の中で変数を宣言したり様々な処理をすることができます。変数 url の値を src 属性に渡すには、src={url} のように書きます。{} の中には、JavaScript の式が書けるため、変数だけでなく計算や関数呼び出しも可能です。

また、属性だけでなく要素の内容(Content)にも {} で、JavaScript の式を埋め込むことができます。

function Calc() {
  const x = 6;
  const y = 7;
  return (
    <p>
      {x} * {y} = {x * y}
    </p>
  );
}

コンポーネントと props

{} で JavaScript の式を JSX に埋め込むことができることがわかりました。次のステップを考えましょう。Image コンポーネントは、1 枚の画像を表示するためのコンポーネントです。犬画像ギャラリーでは、1 枚だけではなくて複数の画像を同時に表示したいです。そのためには、Dog API から複数の画像の URL を取得し、それぞれの URL を Image コンポーネントに渡して画像を表示する必要があります。そこで、次は親コンポーネントから子コンポーネントへプロパティを渡す方法を扱います。

コンポーネントはただの JavaScript の関数であったことを思い出しましょう。呼び出し側の関数である親コンポーネントから、呼び出される側の関数である子コンポーネントへ props を通じてプロパティを渡すことができます。props は子コンポーネントの関数の引数となります。

先ほどはImage コンポーネントの中にあった url 変数をGallery コンポーネントに移して、Gallery コンポーネントからImage コンポーネントへと画像の URL を渡すようにしましょう。Image コンポーネントは、src という名前で画像の URL を受け取ることにします。src/App.jsxを以下のように修正してみましょう。

src/App.jsx
-function Image() {
-  const url = "https://images.dog.ceo/breeds/shiba/shiba-8.jpg";
+function Image(props) {
   return (
     <div className="card">
       <div className="card-image">
         <figure className="image">
-          <img src={url} alt="cute dog!" />
+          <img src={props.src} alt="cute dog!" />
         </figure>
       </div>
     </div>
   );
 }

 function Gallery() {
+  const url = "https://images.dog.ceo/breeds/shiba/shiba-8.jpg";
   return (
     <div className="columns is-vcentered is-multiline">
       <div className="column is-3">
-        <Image />
+        <Image src={url} />
       </div>
     </div>
   );
 }

親コンポーネントから子コンポーネントへプロパティを渡すには、HTML の属性を書くのと同じように プロパティ名={値} または プロパティ名="文字列" の形で書きます。後者の""で渡せるのは文字列のみとなります。JavaScript の式を渡したい場合は{}を使ってください。子コンポーネントでは、関数の仮引数の props を通じて渡されたプロパティにアクセスすることができます。

繰り返し

まだ見た目に変化はありませんが、プログラムは徐々に完成形に近づいてきました。これまでは 1 枚の画像のみを表示していましたが、Dog API から複数の画像の URL を取得してそれを一度に表示してみることを考えてみましょう。URL は以下のように配列になっているはずです。

const urls = [
  "https://images.dog.ceo/breeds/shiba/shiba-11.jpg",
  "https://images.dog.ceo/breeds/shiba/shiba-12.jpg",
  "https://images.dog.ceo/breeds/shiba/shiba-14.jpg",
  "https://images.dog.ceo/breeds/shiba/shiba-17.jpg",
  "https://images.dog.ceo/breeds/shiba/shiba-2.jpg",
  "https://images.dog.ceo/breeds/shiba/shiba-3i.jpg",
  "https://images.dog.ceo/breeds/shiba/shiba-4.jpg",
  "https://images.dog.ceo/breeds/shiba/shiba-5.jpg",
  "https://images.dog.ceo/breeds/shiba/shiba-6.jpg",
  "https://images.dog.ceo/breeds/shiba/shiba-7.jpg",
  "https://images.dog.ceo/breeds/shiba/shiba-8.jpg",
  "https://images.dog.ceo/breeds/shiba/shiba-9.jpg",
];

この配列に含まれる URL それぞれを Image コンポーネント(とそれを囲む div 要素)に割り当てなければいけません。URL の配列を JSX 式の配列に変換すると考えると良いでしょう。このような処理は配列の map メソッドでできました。MainコンポーネントとGarllery コンポーネントを以下のように書き換えてみましょう。

src/App.jsx
-function Gallery() {
-  const url = "https://images.dog.ceo/breeds/shiba/shiba-8.jpg";
+function Gallery(props) {
+  const { urls } = props;
   return (
     <div className="columns is-vcentered is-multiline">
-      <div className="column is-3">
-        <Image src={url} />
-      </div>
+      {urls.map((url) => {
+        return (
+          <div key={url} className="column is-3">
+            <Image src={url} />
+          </div>
+        );
+      })}
     </div>
   );
 }

 function Main() {
+  const urls = [
+    "https://images.dog.ceo/breeds/shiba/shiba-11.jpg",
+    "https://images.dog.ceo/breeds/shiba/shiba-12.jpg",
+    "https://images.dog.ceo/breeds/shiba/shiba-14.jpg",
+    "https://images.dog.ceo/breeds/shiba/shiba-17.jpg",
+    "https://images.dog.ceo/breeds/shiba/shiba-2.jpg",
+    "https://images.dog.ceo/breeds/shiba/shiba-3i.jpg",
+    "https://images.dog.ceo/breeds/shiba/shiba-4.jpg",
+    "https://images.dog.ceo/breeds/shiba/shiba-5.jpg",
+    "https://images.dog.ceo/breeds/shiba/shiba-6.jpg",
+    "https://images.dog.ceo/breeds/shiba/shiba-7.jpg",
+    "https://images.dog.ceo/breeds/shiba/shiba-8.jpg",
+    "https://images.dog.ceo/breeds/shiba/shiba-9.jpg",
+  ];
   return (
     <main>
       <section className="section">
         <div className="container">
-          <Gallery />
+          <Gallery urls={urls} />
         </div>
       </section>
     </main>
   );
 }

Web ページを表示すると、配列に格納された 12 枚分の画像が以下のように表示されるようになるでしょう。

複数画像の表示結果

map メソッドで作られる JSX 式は、最も外側の要素に key 属性を付けなければいけません。<div key={url} className="column is-3"> では、url の値を key としています。key 属性は、繰り返される要素の間で重複しない値を割り当てる必要があります。本稿では扱いませんが、CSS のアニメーションをする際などには適切に key を設定する必要があります。まずは、map メソッドを使う場合には、忘れずに key 属性を付ける習慣を付けておきましょう。

ところで、JSX 式は変数に代入することもできます。以下の例を見てみましょう。

function Header() {
  return <h1>hello</h1>;
}

function App() {
  const header = <Header />;
  return <div>{header}</div>;
}

JSX 式が代入された変数は JSX 式の中に埋め込むことで、ページにレンダリングされます。

また、JSX 式を配列に格納することができます。すなわち、先ほどの Gallery コンポーネントは以下のように書くのと同じ意味を持っています。

function Gallery() {
  const images = [
    <div
      key="https://images.dog.ceo/breeds/shiba/shiba-11.jpg"
      className="column is-3"
    >
      <Image src="https://images.dog.ceo/breeds/shiba/shiba-11.jpg" />
    </div>,
    <div
      key="https://images.dog.ceo/breeds/shiba/shiba-12.jpg"
      className="column is-3"
    >
      <Image src="https://images.dog.ceo/breeds/shiba/shiba-12.jpg" />
    </div>,
    //...
  ];
  return <div className="columns is-vcentered is-multiline">{images}</div>;
}

条件分岐

Dog API などの Web API からのデータ取得は非同期的に行われます。すなわち、Web ページを開いた瞬間にはまだ画像 URL のリストを持っていなくて、Dog API からの応答を受け取ってはじめて画像の一覧を表示することができるようになります。そのため、画像を取得し終わるまでは別の内容を表示しておかなければいけません。例えば、「loading」などと表示しておくと、まだデータを読み込み中であることがわかるでしょう。

さて、データの取得が終わっているかどうかに応じて画面の表示内容を切り替える必要が出てきました。条件分岐によってそれを実現しましょう。まだデータ取得のことは考えず、変数 urls の値が null になっているとしておきます。

ロード中の表示をする Loading コンポーネントを追加し、urlsnull だったら Loading コンポーネントを表示するように、src/App.jsx を以下のように書き換えてみましょう。

src/App.jsx
+function Loading() {
+  return <p>Loading...</p>;
+}
+
 function Gallery(props) {
   const { urls } = props;
+  if (urls == null) {
+    return <Loading />;
+  }
   return (
     <div className="columns is-vcentered is-multiline">
       {urls.map((url) => {
         return (
           <div key={url} className="column is-3">
             <Image src={url} />
           </div>
         );
       })}
     </div>
   );
 }

 function Main() {
-  const urls = [
-    "https://images.dog.ceo/breeds/shiba/shiba-11.jpg",
-    "https://images.dog.ceo/breeds/shiba/shiba-12.jpg",
-    "https://images.dog.ceo/breeds/shiba/shiba-14.jpg",
-    "https://images.dog.ceo/breeds/shiba/shiba-17.jpg",
-    "https://images.dog.ceo/breeds/shiba/shiba-2.jpg",
-    "https://images.dog.ceo/breeds/shiba/shiba-3i.jpg",
-    "https://images.dog.ceo/breeds/shiba/shiba-4.jpg",
-    "https://images.dog.ceo/breeds/shiba/shiba-5.jpg",
-    "https://images.dog.ceo/breeds/shiba/shiba-6.jpg",
-    "https://images.dog.ceo/breeds/shiba/shiba-7.jpg",
-    "https://images.dog.ceo/breeds/shiba/shiba-8.jpg",
-    "https://images.dog.ceo/breeds/shiba/shiba-9.jpg",
-  ];
+  const urls = null;
   return (
     <main>
       <section className="section">
         <div className="container">
           <Gallery urls={urls} />
         </div>
       </section>
     </main>
   );
 }

ページの表示は以下のようになるでしょう。

ロード中の画面

関数が返す JSX 式を条件分岐によって変えることで、ページの表示内容を変えることに成功しました。urls を元の URL の配列に戻せば元の表示に戻ることも確認できるでしょう。

サーバーからのデータ取得

さて、いよいよ Dog API から画像データを取得してみましょう。 https://dog.ceo/api/breed/shiba/images/random/12 に Web ブラウザでアクセスしてみると、以下のように JSON 形式のデータが返されていることがわかります。

Dog APIのレスポンス

これを JavaScript のプログラムから取得します。API からデータを取得するようなロジックは、JSX を組み立てるビューとは分けておくと良いでしょう。新たに src/api.js を作成し、そこに Dog API から画像取得を行う fetchImages 関数を作成します。

src/api.js
export async function fetchImages(breed) {
  const response = await fetch(
    `https://dog.ceo/api/breed/${breed}/images/random/12`
  );
  const data = await response.json();
  return data.message;
}

fetchImages 関数は、犬種の文字列を受け取り、fetch 関数を使って Dog API からその犬種の画像 URL のリストを取得します。コンポーネントから fetchImages 関数を利用しますが、どのようにしたら良いでしょうか?これを考えるには、コンポーネントの主作用と副作用について理解しておく必要があります。

React のコンポーネントは、アプリケーションの状態が同じであれば常に同じ JSX 式を結果として返すべきです。コンポーネントは関数として表されていました。コンポーネントを、アプリケーションの状態を引数として受け取って、その状態に対応したユーザーインタフェースを表す JSX を返す関数だと捉えてみましょう。このとき、アプリケーションの状態から JSX を組み立てることがこの関数の主作用となります。しかし、Web アプリケーションでは、アプリケーションの状態を更新するための副作用も発生します。例えば、外部のサーバーにリクエストを送ることなどが副作用にあたります。

useEffect 関数は、時間の経過や外部サーバーからのリソース取得結果を処理するなど、コンポーネントの副作用を表現するために使われます。小難しい説明は置いておいて、Gallery コンポーネントの中で useEffect を使い、実際に Dog API からのデータ取得をやってみます。まずは src/App.jsx の先頭を以下のように書き換えて、useEffectfetchImages をインポートしましょう。

src/App.jsx
+import { useEffect } from "react";
+import { fetchImages } from "./api";

続いて、Main コンポーネントの中で useEffect を使うように、以下のように書き換えてみましょう。

src/App.jsx
 function Main() {
   const urls = null;
+  useEffect(() => {
+    fetchImages("shiba").then((urls) => {
+      console.log(urls);
+    });
+  }, []);
   return (
     <main>
       <section className="section">
         <div className="container">
           <Gallery urls={urls} />
         </div>
       </section>
     </main>
   );
 }

useEffect の第 1 引数には、副作用を起こす関数を渡します。この関数は、副作用をクリーンアップする処理をする関数を戻り値として返すことができます。クリーンアップが必要ない場合は、戻り値を返す必要がありません。

useEffect の第 2 引数には、その副作用が依存する値のリストを配列で渡します。この配列のいずれかの値が、前に副作用を起こした時の値から変わっていたら、再度副作用を起こします。空の配列を渡した場合は、最初にコンポーネントがレンダリングされた時の 1 回だけ副作用が起こされます。第 2 引数を省略すると、コンポーネントの再レンダリングのたびに副作用が起こされます。

今回は、第 1 引数の関数の中で fetchImages を呼び出し、Dog API から取得した URL のリストをコンソールに表示します。データの取得は、Web ページにアクセスしたときに 1 回行えば十分です。このような場合には、第 2 引数に空引数を渡しましょう。

取得した URL のリストを使って画像を表示するにはあと一歩が必要です。焦らずにいきましょう。Web ページにアクセスして開発者ツールを開くと、以下のようにコンソールにメッセージが表示されているのを確認できるでしょう。

コンソールの表示結果

状態の変更

犬画像ギャラリーでは、最初は画像 URL のリストを持っておらず、Dog API から結果を受け取ったらそれを画像 URL のリストとして画面を更新します。すなわちこのアプリケーションでは、画像 URL のリストという状態が最初は null で、API からの取得が完了すると URL の配列になるわけです。React では、これをコンポーネントの状態の更新によって実現します。

コンポーネントの状態を扱うためには useState 関数を使います。まずは、src/App.jsx の先頭で useState をインポートしましょう。

src/App.jsx
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
 import { fetchImages } from "./api";

次に、Main コンポーネントを以下のように書き換えます。

src/App.jsx
 function Main() {
-  const urls = null;
+  const [urls, setUrls] = useState(null);
   useEffect(() => {
     fetchImages("shiba").then((urls) => {
-      console.log(urls);
+      setUrls(urls);
     });
   }, []);
   return (
     <main>
       <section className="section">
         <div className="container">
           <Gallery urls={urls} />
         </div>
       </section>
     </main>
   );
 }

ページを表示すると、無事に かわいい 犬の画像が表示されるでしょう。Dog API からランダムに 12 枚の画像を取得しているので、ページをリロードすると違う画像が表示されるはずです。

useState の引数は状態の初期値です。useState の戻り値は 2 要素の配列であり、0 番目の要素は現在の状態の値、1 番目の要素は状態を更新するための関数です。状態を更新するたびにコンポーネントの関数が実行され、現在の状態に対応したが JSX 式が返されます。

フォームの操作とイベントハンドリング

犬画像ギャラリーに最後にもう 1 つ機能を追加しましょう。今は柴犬の画像のみを表示していますが、Dog API は様々な犬種の画像を提供しています。表示する画像の犬種を選べるようにしてみましょう。

まずはフォームのマークアップをしましょう。以下のようにsrc/App.jsx を書き換えて、 Form コンポーネントの追加とMain コンポーネントの変更を行いましょう。

src/App.jsx
+function Form() {
+  return (
+    <div>
+      <form>
+        <div className="field has-addons">
+          <div className="control is-expanded">
+            <div className="select is-fullwidth">
+              <select name="breed" defaultValue="shiba">
+                <option value="shiba">Shiba</option>
+                <option value="akita">Akita</option>
+              </select>
+            </div>
+          </div>
+          <div className="control">
+            <button type="submit" className="button is-dark">
+              Reload
+            </button>
+          </div>
+        </div>
+      </form>
+    </div>
+  );
+}
+
 function Main() {
   const [urls, setUrls] = useState(null);
   useEffect(() => {
     fetchImages().then((urls) => {
       setUrls(urls);
     });
   }, []);
   return (
     <main>
+      <section className="section">
+        <div className="container">
+          <Form />
+        </div>
+      </section>
       <section className="section">
         <div className="container">
           <Gallery urls={urls} />
         </div>
       </section>
     </main>
   );
 }

続いて、フォームが送信されたときの処理を追加します。form要素において、フォームが送信されるときには submit イベントが発生します。React では、イベントを起こす要素にイベントを処理する関数を渡すことで行います。submit イベントを処理するためには、onSubmitform 要素に追加します。まず submit イベントを処理する関数を作りましょう。名前を handleSubmit としておきます。handleSubmit 関数は、イベントを引数として受け取り、イベント発生時の処理を行います。今回は、select要素で選択された値を引数として親コンポーネントから渡された onFormSubmit 関数を呼び出すことにします。また、React で作るようなシングルページアプリケーション(SPA; Single Page Application)では、submit イベントのデフォルトの振る舞いを止めるために event.preventDefault() を呼び出します。デフォルトの振る舞いではフォームを送信した後にページのリロードが行われるため、アプリケーションの状態がページアクセス時にリセットされてしまうためです。そして、handleSubmit 関数を form 要素のonSubmit に渡します。最終的にForm コンポーネントは以下のようになります。

src/App.jsx
-function Form() {
+function Form(props) {
+  function handleSubmit(event) {
+    event.preventDefault();
+    const { breed } = event.target.elements;
+    props.onFormSubmit(breed.value);
+  }
   return (
     <div>
-      <form>
+      <form onSubmit={handleSubmit}>
         <div className="field has-addons">
           <div className="control is-expanded">
             <div className="select is-fullwidth">
               <select name="breed" defaultValue="shiba">
                 <option value="shiba">Shiba</option>
                 <option value="akita">Akita</option>
               </select>
             </div>
           </div>
           <div className="control">
             <button type="submit" className="button is-dark">
               Reload
             </button>
           </div>
         </div>
       </form>
     </div>
   );
 }

次に、select要素で選択された値をForm コンポーネントから受け取るようにMain コンポーネントを書き換えていきます。Formコンポーネントは、フォームが送信されたときに、select要素の値を引数としてonFormSubmit 関数を呼び出すのでした。select要素の値を受け取って処理する関数を reloadImages として作成します。関数の中身では、fetchImages 関数を呼び出して新しく取得した画像 URL のリストでurlsを更新します。このreloadImagesForm コンポーネントのonFormSubmit プロパティに渡しましょう。Mainコンポーネントは以下のようになります。

src/App.jsx
 function Main() {
   const [urls, setUrls] = useState(null);
   useEffect(() => {
     fetchImages("shiba").then((urls) => {
       setUrls(urls);
     });
   }, []);
+  function reloadImages(breed) {
+    fetchImages(breed).then((urls) => {
+      setUrls(urls);
+    });
+  }
   return (
     <main>
       <section className="section">
         <div className="container">
-          <Form />
+          <Form onFormSubmit={reloadImages} />
         </div>
       </section>
       <section className="section">
         <div className="container">
           <Gallery urls={urls} />
         </div>
       </section>
     </main>
   );
 }

ページを表示するとselect要素とbutton要素を持ったフォームが追加されているでしょう。最初は柴犬の画像が表示されていますが、select要素で「Akita」を選び、「Reload」ボタンを押すと以下のように秋田犬の画像が表示されるでしょう。(現在 Dog API で提供されている秋田県の画像は 12 枚に満たないようで、9 枚だけ表示されます。)

犬画像ギャラリーの完成

React のイベントハンドリングでは、submit 以外にも、onClickonChange などの属性に関数を渡すことで、対応するイベントの処理ができます。属性名は on に UpperCamelCase のイベント名をつけたものになります。

Web アプリの公開

仕上げに、開発した すばらしい 犬画像ギャラリーを、世界中の人がアクセスできるように公開しましょう。Web アプリの公開にはいくつもの方法がありますが、今回はNetlify を利用します。Netlify を使えば実用的な Web アプリを無料で公開できます。Netlify のアカウントを持っていない人は新しく作成してください。

ここでは Netlify で Web アプリを公開する最も簡単な方法を紹介します。まずは、犬画像ギャラリーを公開用にプロダクションビルドしましょう。プロダクションビルドとは、開発用のデバッグ情報を取り除いたり、ソースコードの圧縮を行ったりして公開に向けた最適化を行うことです。プロダクションビルドには以下のコマンドを実行します。ローカル開発サーバーを起動していた場合は停止してからコマンドを実行しましょう。

$ npm run build

プロダクションビルドが完了すると、dist ディレクトリの中に公開用ファイルが保存されます。Netlify にログインし、「Sites」タブを開きましょう。以下のように、画面の下部に「Want to deploy a new site without connecting to Git? Drag and drop your site folder here」と表示されているでしょう。指示通り、dist ディレクトリをここにドラッグアンドドロップしましょう。

Netlifyの管理画面

以下のようにページが切り替わり、Web アプリの公開が完了しました!書かれている URL にアクセスし、公開した Web アプリがうまく動作しているか確認しましょう。

公開完了の画面

もし不具合があったり機能を追加したくなったら、まずはローカル開発サーバーを使って Web アプリの更新を行いましょう。更新ができたら上と同じようにプロダクションビルドを行ってください。Netlify 上で、公開した Web アプリの管理ページで「Deploys」タブを開きます。以下のように、「Need to update your site? Drag and drop your site folder here」と書かれている欄があるでしょう。最初の公開時と同じように、ここに dist ディレクトリをドラッグアンドドロップしましょう。

更新の画面

おわりに

本稿では簡単な Web アプリケーションを作りながら React の使い方を学びました。最後まで完成させることができたでしょうか?躓いてしまった人は、もう一度説明を読み直してうまくいかない原因を探してみましょう。友達やまわりの人と相談しながら進めてみるのも手かもしれません。うまく完成できた人は、「もっとこんなことができるんじゃないか?」と思い浮かんだかもしれません。学習した知識を活かして、そのアイデアを実現できるか試してみると良いでしょう。

本稿では React の基本のみを扱っています。より詳しく React を学ぶためには、React の公式ドキュメント を読んでみてください。React 公式のチュートリアルにチャレンジするのも良いでしょう。

おまけ

本稿では扱わなかった JSX の特殊なルールをいくつか紹介しておきます。

children

子コンポーネントの子要素として書かれた要素は、子コンポーネントに children という名前で渡されます。

function Section(props) {
  return (
    <section className="section">
      <div className="container">{props.children}</div>
    </section>
  );
}

function App() {
  return (
    <Section>
      <h1>Title</h1>
      <p>hello</p>
    </Section>
  );
}

style

JSX の style 属性には、スタイル名と値のペアをオブジェクトとして渡すことができます。

function App() {
  return (
    <div
      style={{
        width: "600px",
        margin: "0 auto",
        backgroundColor: "#FF9500",
        padding: "0 20px 20px 20px",
        border: "5px solid black",
      }}
    />
  );
}

命名規則

HTML、CSS で通常は kebab-case で表される名前は、JSX の属性または style の名前では lowerCamelCase にする必要があります。コンポーネントのプロパティも lowerCamelCase が推奨されます。

function UserInfo(props) {
  return (
    <div style={{ marginBottom: "2rem" }}>
      <p>
        {props.firstName} {props.lastName}
      </p>
      <p>
        <a href={`mailto:${props.email}`}>contact</a>
      </p>
    </div>
  );
}

function App() {
  const users = [
    { firstName: "Yosuke", lastName: "Onoue", email: "onoue@example.com" },
    {
      firstName: "Tetsuro",
      lastName: "Kitahara",
      email: "kitahara@example.com",
    },
  ];
  return (
    <div style={{ backgroundColor: "#ccc" }}>
      {users.map((user) => {
        return (
          <UserInfo
            firstName={user.firstName}
            lastName={user.lastName}
            email={user.email}
          />
        );
      })}
    </div>
  );
}

コンポーネント名に規則はありませんが、一般的には UpperCamelCase が使われます。

Discussion