Reactチュートリアル1:犬画像ギャラリーを作ろう
本資料について
本資料は日本大学文理学部情報科学科の開講科目「Web プログラミング」の教材として作成されました。本資料は下記のライセンスの範囲内で、当授業以外でも自由にご利用いただけます。
対象読者
本資料は、以下の教材を学習済み、もしくはそれと同等以上の知識を持っていることを前提としています。
本資料で学ぶこと
本資料では以下の内容を学びます。
- React の基本
- 開発の始め方
- JSX
- コンポーネントと props
- 条件分岐と繰り返し
- useEffect による副作用の扱い
- useState による状態管理
- フォームとイベントハンドリング
- Netlify による Web アプリの公開
ライセンス
この作品はクリエイティブ・コモンズ 表示 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
となっています。
{
"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 を使うので、react
や react-dom
というパッケージに依存することになります。また、React アプリケーションのビルドや、ローカル開発サーバーの起動に vite
というパッケージを使います。react-tutorial
は vite
にも依存することになります。
パッケージの依存関係にはいくつかの種類があります。その種類の主要なものとして、アプリケーションが使用するライブラリの依存関係と、アプリケーションを開発するために使うツールの依存関係があります。react
と react-dom
は前者に、vite
は後者に該当します。
依存するパッケージをインストールし管理するために npm install
コマンドを使用します。
まずは、アプリケーションが使用するライブラリである react
と react-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
に含まれます。
{
"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 と呼びます。
{
"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 によるソースコードの差分表記に読み慣れておきましょう。
{
"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.js
と index.html
を、src
の中に App.jsx
と main.jsx
をそれぞれ以下の内容で作成します。
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
});
<!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>
function App() {
return (
<div>
<h1>Hello, World!</h1>
</div>
);
}
export default App;
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 の世界へようこそ!うまくいかなかったら、これまでの手順をよく見直してやり直してみてください。
完成イメージ
これから 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.html
のbody
要素の中には、content
を id
に持った div
要素(#content
と呼びましょう)が含まれています。src/main.jsx
が src/App.jsx
からインポートした App
コンポーネントを、#content
にマウントすることで、App
コンポーネントが組み立てた JSX 式によってレンダリングされた HTML を Web ブラウザ上に表示します。
さて、犬画像ギャラリーの開発に戻ります。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
の先頭を以下のように書き加えましょう。
+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
を以下のように書き直します。
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 のドキュメント を眺めてみて、面白そうな表現があったら試してみると良いでしょう。
コンポーネントの分割
さて、見た目を整えるためにApp
コンポーネントに要素をたくさん書いてきましたが、App
コンポーネントの中身が随分長くなってしまいました。こういうときはコンポーネントの分割をしましょう。
HTML は階層構造を持ちますが、あるコンポーネントの JSX 式の中に別のコンポーネントを書くことで、コンポーネントも階層化することができます。コンポーネントを使う側のコンポーネントを 親コンポーネント、別のコンポーネントから使われる側のコンポーネントを 子コンポーネント と呼びます。
App
コンポーネントの中身を、以下のようにHeader
、Image
、Gallery
、Main
、Footer
の 5 つのコンポーネントに分割してみましょう。
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
コンポーネントを以下のように書き換えます。
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
を以下のように修正してみましょう。
-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
コンポーネントを以下のように書き換えてみましょう。
-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
コンポーネントを追加し、urls
が null
だったら Loading
コンポーネントを表示するように、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 形式のデータが返されていることがわかります。
これを JavaScript のプログラムから取得します。API からデータを取得するようなロジックは、JSX を組み立てるビューとは分けておくと良いでしょう。新たに src/api.js
を作成し、そこに Dog API から画像取得を行う fetchImages
関数を作成します。
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
の先頭を以下のように書き換えて、useEffect
と fetchImages
をインポートしましょう。
+import { useEffect } from "react";
+import { fetchImages } from "./api";
続いて、Main
コンポーネントの中で useEffect
を使うように、以下のように書き換えてみましょう。
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
をインポートしましょう。
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
import { fetchImages } from "./api";
次に、Main
コンポーネントを以下のように書き換えます。
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
コンポーネントの変更を行いましょう。
+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
イベントを処理するためには、onSubmit
をform
要素に追加します。まず submit
イベントを処理する関数を作りましょう。名前を handleSubmit
としておきます。handleSubmit
関数は、イベントを引数として受け取り、イベント発生時の処理を行います。今回は、select
要素で選択された値を引数として親コンポーネントから渡された onFormSubmit
関数を呼び出すことにします。また、React で作るようなシングルページアプリケーション(SPA; Single Page Application)では、submit
イベントのデフォルトの振る舞いを止めるために event.preventDefault()
を呼び出します。デフォルトの振る舞いではフォームを送信した後にページのリロードが行われるため、アプリケーションの状態がページアクセス時にリセットされてしまうためです。そして、handleSubmit
関数を form
要素のonSubmit
に渡します。最終的にForm
コンポーネントは以下のようになります。
-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
を更新します。このreloadImages
を Form
コンポーネントのonFormSubmit
プロパティに渡しましょう。Main
コンポーネントは以下のようになります。
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
以外にも、onClick
や onChange
などの属性に関数を渡すことで、対応するイベントの処理ができます。属性名は 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
ディレクトリをここにドラッグアンドドロップしましょう。
以下のようにページが切り替わり、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