logo
コーディング

更新日

React + Unsplash APIで画像検索アプリを作ろう

Reactの勉強がてら、高画質な画像を配布しているUnsplashが提供しているUnsplash APIを使って画像検索アプリを作ってみました。その復習に作成手順をまとめてみたので、これからReactを勉強しよう!と思っている方の役に立てれば幸いです!

この記事は動画でも解説しています。動画派の方はぜひご覧ください!

目次

Unsplash とは

Unsplashは無料で商用利用もできる写真素材サイト。高解像度でおしゃれな写真ばかりで、いつもお世話になっています!Unsplash では登録されている画像を利用したアプリを開発できるよう、API が公開されています。詳しい仕様は公式ドキュメントを参照してください。

こんなアプリを作ってみよう

検索ボックスにキーワードを入力すると、Unsplash に登録されている画像が表示されるアプリを作ってみましょう。(検索できるのは Unsplash の仕様により英単語がメインになります)

開発時や学習用にはデモ版(Demo)が利用できます。デモ版では 1 時間に 50 リクエストまで対応されています。製品版(Production)では 1 時間に 5000 リクエストまで対応。アプリが完成したら申請して承認されると製品版に移行できます。

今回作成したコードはGitHubで公開しています!参考になれば幸いです!

1. Unsplash API の開発者登録

Unsplash API を利用するために、まずは開発者用アカウントの登録をしましょう。API ページの右上「Register as a developer」ボタンから登録開始です。

名前やメールアドレス、ユーザー名、パスワードを入力して「Join」。

登録完了です!さっそく新しいアプリを作成するため、「New Application」をクリックします。

API の利用ガイドラインページに遷移します。よく読んで同意したらチェックを入れ、「Accept terms」ボタンをクリック。

「Application name」にアプリの名前、ここでは「Image Search App」と入力し、アプリの詳細を簡単に入力したら「Create application」ボタンをクリック。

アプリの管理画面が作成されました!ページ下の方に表示される「Keys」の Access Key は開発時に利用します。

2. Vite で React アプリのベースを作成

今回はViteを使って React のプロジェクトを作成します。プロジェクトを作成したいフォルダーを開き、ターミナルで

npm create vite@latest

と入力。プロジェクト名は「image-search-app」とします。「Done.」と表示されたら完成です!フォルダーを確認すると「image-search-app」という新規フォルダーが作成されていて、ファイルができあがっています。続いて、

cd image-search-app

でフォルダーを移動し、

npm install

で、動作させるための必要なパッケージをインストールします。

npm run dev

を入力すると、デフォルトの画面が表示されるようになります。

詳しい説明は過去記事「Vite + React で新規プロジェクトの開発環境を作ろう」を読んでみてください!

不要なファイルの削除

デフォルトの雛形から、今回は利用しないファイルや記述を削除しておきましょう。まずは「src」フォルダー内の以下のファイルを削除します:

  • favicon.svg
  • index.css
  • logo.svg

するとこんな構成になります。

不要なコードの削除

続いて記述されているコードから不要なものを削除し、最終的に以下の内容になっているよう編集します。

App.jsx

import "./App.css";

function App() {
  return <div className="App">Hello!</div>;
}

export default App;

Main.jsx

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";

ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

App.css

body {
  text-align: center;
}

これでhttp://localhost:3000/にアクセスすると、こんな感じで「Hello!」とだけ表示されるはずです!

3. タイトル部分の作成(Title.jsx)

React ではパーツごとにファイルを分割し、コンポーネントとして扱ってページを構成します。今回のアプリではコンポーネントをこんな感じで構成します。まずはシンプルなタイトル部分をコンポーネントとして作成・読み込みをして、各パーツをどのようにして組み合わせていくのかを理解していきましょう。

「src」フォルダーの中に「components」フォルダーを作成します。そしてその「components」フォルダーの中に新規ファイル「Title.jsx」を作成しましょう。return のカッコ内には通常の HTML と同じようなタグを書き込めます。

Title.jsx

const Title = () => {
  return (
    <header>
      <h1>Image Search App</h1>
      <p>
        By <a href="https://unsplash.com/">Unsplash</a>
      </p>
    </header>
  );
};

export default Title;

ただ、これだけだとパーツを作成しただけなので、ページ内には反映されません。この Title パーツをどこで使うのかを指定する必要があります。この Title は App.jsx で読み込ませましょう。

App.jsx

import Title from "./components/Title"; // ← 追加
import "./App.css";

function App() {
  return (
    <div className="App">
      <Title /> // ← 追加
    </div>
  );
}

export default App;

確認するとこんな感じでタイトル部分が表示されました!

4. 検索フォームの作成(Form.jsx)

続いて検索するためのフォームを作っていきます。「components」フォルダー内に「Form.jsx」というファイルを新規作成しましょう。return の中には入力フォームとボタンを用意します。

Form.jsx

const Form = () => {
  return (
    <form>
      <input type="text" name="keyword" placeholder="e.g. cat" />
      <button type="submit">Search</button>
    </form>
  );
};

export default Form;

そして、Title コンポーネントと同様、Form も App.jsx で読み込ませます。

App.jsx

import Title from "./components/Title";
import Form from "./components/Form"; // ← 追加
import "./App.css";

function App() {
  return (
    <div className="App">
      <Title />
      <Form /> // ← 追加
    </div>
  );
}

export default App;

これでページ内に検索フォームが表示されました。しかしこの時点では入力したデータを送る仕組みがないので、なにか入力してボタンを押しても何も起こりません。そこで state と呼ばれる保管場所に入力したキーワードを入れておき、そのキーワードを管理したり操作できるようにします。

まずはその保管場所 state を使えるように React から state を読み込みます。

App.jsx

import { useState } from "react"; // ← 追加
import Title from "./components/Title";
import Form from "./components/Form";
import "./App.css";

function App() {
  return (
    <div className="App">
      <Title />
      <Form />
    </div>
  );
}

export default App;

続いてユーザーが入力したキーワードを保存する word と、それを操作する setWord を用意します。ここの名前は任意なんですが、通例として二番目に書くものは「set + State 名」にすることが多いようです。useState のカッコ内は初期データを入れられますが、今回は '' として空の状態にしています。何か初期データを入れておきたいときは useState('cat') みたいに書いておくといいです。

App.jsx

import { useState } from "react";
import Title from "./components/Title";
import Form from "./components/Form";
import "./App.css";

function App() {
  const [word, setWord] = useState(""); // ← 追加

  return (
    <div className="App">
      <Title />
      <Form />
    </div>
  );
}

export default App;

この state を Form.jsx に渡すため、Form コンポーネントの読み込み箇所を <Form setWord={setWord} /> と変更しておきます。

App.jsx

import { useState } from "react";
import Title from "./components/Title";
import Form from "./components/Form";
import "./App.css";

function App() {
  const [word, setWord] = useState("");

  return (
    <div className="App">
      <Title />
      <Form setWord={setWord} /> // ← setWord 追加
    </div>
  );
}

export default App;

続いて Form.jsx 側にも state を管理するための記述をしていきましょう。まずは一行目で「setWord を使うよー」と伝えておきます。そしてフォームに入力されたテキストを setWord に入れたいので、 onChange を使ってつなげます。入力されたテキストは e.target.value にあるので、それを setWord に保管する、という指定ですね。

Form.jsx

const Form = ({ setWord }) => {
  return (
    <form>
      <input
        type="text"
        name="keyword"
        placeholder="e.g. cat"
        onChange={(e) => setWord(e.target.value)} // ← 追加
      />
      <button type="submit">Search</button>
    </form>
  );
};

export default Form;

試しに App.jsx に {word} と追加してどうなるか確認してみましょう。

App.jsx

import { useState } from "react";
import Title from "./components/Title";
import Form from "./components/Form";
import "./App.css";

function App() {
  const [word, setWord] = useState("");

  return (
    <div className="App">
      <Title />
      <Form setWord={setWord} />
      {word} // ← 追加。確認できたら消す。
    </div>
  );
}

export default App;

入力されたテキストがそのまま表示されていれば成功です!この {word} は確認のためだけなので、うまく表示されたなら消してもらって OK です。

5. Unsplash のデータを取得

ユーザーが入力したキーワードの管理ができるようになりましたが、さらにそのキーワードから Unsplash の画像が検索できるようにしましょう。ここでは非同期通信でデータを簡単に取得できる「axios」という JavaScript のライブラリーを使います。

まずは axios をインストールします。インストールするためにはターミナルを使うので、一旦 Ctrl + C で Vite を停止させましょう。そして以下のコマンドを入力します。

npm install axios

完了したら「package.json」を開きます。axios が追加されているのがわかりますね。

それでは App.jsx を開いて axios を読み込ませます。

App.jsx

import { useState } from "react";
import axios from "axios"; // ← 追加
import Title from "./components/Title";
import Form from "./components/Form";
import "./App.css";

function App() {
  const [word, setWord] = useState("");

  return (
    <div className="App">
      <Title />
      <Form setWord={setWord} />
    </div>
  );
}

export default App;

ボタンをクリックしたら Unsplash からデータを取得するための関数を用意します。ここでは「getPhotoData」としました。.get に API のアドレスを記述します。Unpslash の API は https://api.unsplash.com で、検索用には末尾に /search/photos をつけ、パラメターに検索ワードやアクセスキーを追加します。つまり以下のような構図:

https://api.unsplash.com/search/photos?query=検索ワード&client_id=アクセスキー

「検索ワード」の部分に、入力された word が入るわけですね。この設定は後ほどやるとして、ひとまず「cat」などのキーワードを入れておきましょう。パラメターは検索ワードの他にも順序や色、画像の方向などを指定できます。詳しくは公式ドキュメントをご覧ください。

アクセスキーは最初に Unsplash の Web サイトから新規アプリを作成したときに表示されていましたね。ご自身の管理ページから確認してください。

取得したデータは .then で受け取ります。res は「response」の略で、ここに Unsplash のデータが格納されています。ちゃんと受け取れているのかコンソールで確認するために、console.log(res) を記述しておきます。

App.jsx

import { useState } from "react";
import axios from "axios";
import Title from "./components/Title";
import Form from "./components/Form";
import "./App.css";

function App() {
  const [word, setWord] = useState("");

  // ↓ 追加
  const getPhotoData = () => {
    axios
      .get("https://api.unsplash.com/search/photos?query=cat&client_id=XXXXX")
      .then((res) => console.log(res));
  };
  // ↑ 追加

  return (
    <div className="App">
      <Title />
      <Form setWord={setWord} />
    </div>
  );
}

export default App;

ボタンをクリックしたら上記の「getPhotoData」関数を動作させたいので、Form コンポーネントに「getPhotoData 使うよー!」と伝えましょう。

App.jsx

import { useState } from "react";
import axios from "axios";
import Title from "./components/Title";
import Form from "./components/Form";
import "./App.css";

function App() {
  const [word, setWord] = useState("");

  const getPhotoData = () => {
    axios
      .get("https://api.unsplash.com/search/photos?query=cat&client_id=XXXXX")
      .then((res) => console.log(res));
  };

  return (
    <div className="App">
      <Title />
      <Form setWord={setWord} getPhotoData={getPhotoData} /> // ← getPhotoData追加
    </div>
  );
}

export default App;

Form.jsx では一行目で「getPhotoData 使うんだねー!」と設定して button をクリックしたら、つまり onClick のときに関数が発動するように記述します。

Form.jsx

const Form = ({ setWord, getPhotoData }) => {
  // ← getPhotoData 追加
  return (
    <form>
      <input
        type="text"
        name="keyword"
        placeholder="e.g. cat"
        onChange={(e) => setWord(e.target.value)}
      />
      <button type="submit" onClick={getPhotoData}>
        Search
      </button> // ← onClick追加
    </form>
  );
};

export default Form;

実際に動作するか確認しましょう。axios をインストールするときにターミナルで Vite を止めていたので、再度 npm run dev で動かします。これでブラウザーで確認できますね。

確認しようとボタンをクリックしたところ、ページが更新されてしまい、コンソールに表示したいデータがうつらなかったかと思います。これはフォームを送信するときのデフォルトの動作。 getPhotoData 関数のパラメターに e を渡して e.preventDefault(); を追加しましょう。これでボタンをクリックしてもページが更新されなくなります。

import { useState } from "react";
import axios from "axios";
import Title from "./components/Title";
import Form from "./components/Form";
import "./App.css";

function App() {
  const [word, setWord] = useState("");

  const getPhotoData = (e) => {
    // ← e 追加
    e.preventDefault(); // ← 追加
    axios
      .get("https://api.unsplash.com/search/photos?query=cat&client_id=XXXXX")
      .then((res) => console.log(res));
  };

  return (
    <div className="App">
      <Title />
      <Form setWord={setWord} getPhotoData={getPhotoData} />
    </div>
  );
}

export default App;

あらためてブラウザーで見てみましょう。ボタンをクリックすると、コンソールにこのような配列が表示されるはずです。data を展開していくと、この中に画像のデータが入っているのがわかりますね!これを検索結果として表示すれば完成です!楽しくなってまいりました!

6. 検索結果の表示(Results.jsx)

検索結果には実際に Unsplash から引っ張ってきた画像を表示させます。そのためのコンポーネントを用意しましょう。「components」フォルダーに「Results.jsx」というファイルを新規作成します。TitleForm と同様、雛形となる部分を記述しましょう。div にはあとでスタイルを適用できるよう、クラスを割り振ります。クラス名は HTML とは違い className で指定する点に注意しましょう。画像はひとまずダミーで用意しておきます。

Results.jsx

const Results = () => {
  return (
    <div className="photo-list">
      <a href="#">
        <img src="https://source.unsplash.com/random" alt="" />
      </a>
    </div>
  );
};

export default Results;

そして App.jsx で Results コンポーネントを読み込ませます。

App.jsx

import { useState } from "react";
import axios from "axios";
import Title from "./components/Title";
import Form from "./components/Form";
import Results from "./components/Results"; // ← 追加
import "./App.css";

function App() {
  const [word, setWord] = useState("");

  const getPhotoData = (e) => {
    e.preventDefault();
    axios
      .get("https://api.unsplash.com/search/photos?query=cat&client_id=XXXXX")
      .then((res) => console.log(res));
  };

  return (
    <div className="App">
      <Title />
      <Form setWord={setWord} getPhotoData={getPhotoData} />
      <Results /> // ← 追加
    </div>
  );
}

export default App;

Results.jsx に追加したダミー画像が表示されました!表示されるダミー画像はランダムで変わるので、この画面と違う画像でも問題ありませんよー!

それでは実際に Unsplash の画像をダミー画像部分に表示させます。Unsplash のデータは App.jsx の res に入っているんでしたね。このデータを state で保管できるようにしましょう。state 名は「photo」としました。データの種類は配列であることがわかっているので、角括弧を使って useState([]) と記述しています。

そして axios で受け取ったデータを作成した state に入れたいので、.then 部分を書き換えます。ブラウザーでボタンをクリックしてコンソールを見ると、受け取る配列データは data の中の results に入っているのがわかるので、res.data.results としましょう。

あとは作成した photo のデータを Results コンポーネントでも利用できるよう、<Results photo={photo} /> と修正しておきましょう。

App.jsx

import { useState } from "react";
import axios from "axios";
import Title from "./components/Title";
import Form from "./components/Form";
import Results from "./components/Results";
import "./App.css";

function App() {
  const [word, setWord] = useState("");
  const [photo, setPhoto] = useState([]); // ← 追加

  const getPhotoData = (e) => {
    e.preventDefault();
    axios
      .get("https://api.unsplash.com/search/photos?query=cat&client_id=XXXXX")
      .then((res) => {
        setPhoto(res.data.results); // ← 追加
      });
  };

  return (
    <div className="App">
      <Title />
      <Form setWord={setWord} getPhotoData={getPhotoData} />
      <Results photo={photo} /> // ← photo 追加
    </div>
  );
}

export default App;

Results では photo のデータを使いたいので、一行目に追加しておきます。そして受け取った配列データをひとつひとつ分解させて繰り返し表示させます。そこで使えるのが map() です。分解された個別データは singleData に入っているので、.(ドット)でつなげて必要な情報を表示させます。

必要なデータはコンソールで確認しておきましょう。console.log(singleData) を入れてボタンをクリックすると、各データが表示されます。

Results.jsx

const Results = ({ photo }) => {
  // ← {photo} 追加
  return (
    <div className="photo-list">
      {photo.map(
        (
          singleData // ← 追加
        ) => console.log(singleData) // ← 追加。確認したら削除。
        // ↓ 一旦コメントアウト。確認したらもとに戻す。
        // <a href="#">
        //     <img src="https://source.unsplash.com/random" alt="" />
        // </a>
        // ↑ 一旦コメントアウト。確認したらもとに戻す。
      )}{" "}
      // ← 追加
    </div>
  );
};

export default Results;

今回は以下の情報が必要になります:

  • 画像ページへのリンク … links.html
  • 画像のパス … urls.regular
  • alt 属性のテキスト … alt_description

必要な情報が確認できたら、リンク先や画像のパス、alt 属性に当てはめましょう。

Results.jsx

const Results = ({ photo }) => {
  return (
    <div className="photo-list">
      {photo.map((singleData) => (
        <a href={singleData.links.html}>
          {" "}
          // ← 変更
          <img
            src={singleData.urls.regular}
            alt={singleData.alt_description}
          />{" "}
          // ← 変更
        </a>
      ))}
    </div>
  );
};

export default Results;

ブラウザーで見てみましょう。ボタンをクリックすると猫の画像が表示されるようになりました!画像をクリックすると Unsplash のページに移動されますね!

ただ、コンソールを確認すると、「Warning: Each child in a list should have a unique “key” prop.」というエラーが出ています。map() を使って HTML タグを表示させたときは、それぞれのタグに key と呼ばれる他とかぶらない個別の番号を加える必要があります。map() では自動的に個別の番号 index が生成されているので、それを key に割り当てましょう。

Results.jsx

const Results = ({ photo }) => {
  return (
    <div className="photo-list">
      {photo.map(
        (
          singleData,
          index // ← index 追加
        ) => (
          <a href={singleData.links.html} key={index}>
            {" "}
            // ← key 追加
            <img
              src={singleData.urls.regular}
              alt={singleData.alt_description}
            />
          </a>
        )
      )}
    </div>
  );
};

export default Results;

これでエラーもなく、無事画像が表示されましたね!ただ、このアプリで表示させたいのは猫の画像だけではありません(それはそれで癒やされますが!)。フォームに入力した単語を検索したいんでしたね。入力されたテキストは {word} の中に入っているので、App.jsx の .get で呼び出しす URL を書き換えます。

文字列だった URL をテンプレートリテラルに書き換えます。``(バックティック)で囲みなおして、「cat」と書いていたところを ${word} に変更しましょう。これだけで query= に入力したテキストが入り、検索したワードの画像が表示されるようになります!

App.jsx

import { useState } from "react";
import axios from "axios";
import Title from "./components/Title";
import Form from "./components/Form";
import Results from "./components/Results";
import "./App.css";

function App() {
  const [word, setWord] = useState("");
  const [photo, setPhoto] = useState([]);

  const getPhotoData = (e) => {
    e.preventDefault();
    axios
      .get(
        `https://api.unsplash.com/search/photos?query=${word}&client_id=XXXXX`
      ) // ← ${word}に変更
      .then((res) => {
        setPhoto(res.data.results);
      });
  };

  return (
    <div className="App">
      <Title />
      <Form setWord={setWord} getPhotoData={getPhotoData} />
      <Results photo={photo} />
    </div>
  );
}

export default App;

ひゃー!動いたー!!検索した単語に関連する画像が表示されましたね!

あとは画像が見づらいので、CSS で調整しちゃいましょう。ここでは簡単に画像をグリッドで並べただけですが、お好みでタイトルやフォーム部分も変更するといいですね!

App.css

body {
  text-align: center;
}
img {
  width: 100%;
}
.photo-list {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: 1rem;
  margin: 1rem;
}

こんな感じに表示されます!

7. 環境変数の作成(.env)

これで完成としてもいいんですが、そういえば Unsplash API のキーを App.jsx にそのまま書いた状態でしたね。キーは非公開にしておきたいので、そんなときは環境変数というものを使います。具体的に言うと同じ階層に .env というファイルを作成し、キーはそこに記述。そして .env で指定した変数を呼び出して利用する、という流れにします。

Vite を使って開発されたプロジェクトは VITE_ から始まる変数のみが反映されます。ここでは VITE_UNSPLASH_API_KEY という変数名にしました。= でつないで API キーを記述します。

.env

VITE_UNSPLASH_API_KEY=XXXXXXXXXXXXXXXXXXX

App.jsx の API キーを記述していた箇所を書き換えましょう。利用するときは import.meta.env.変数名 で呼び出せますよ。

App.jsx

import { useState } from 'react'
import axios from 'axios'
import Title from './components/Title'
import Form from './components/Form'
import Results from './components/Results'
import './App.css'

function App() {
  const [word, setWord] = useState('')
  const [photo, setPhoto] = useState([])

  const getPhotoData = (e) => {
    e.preventDefault();
    axios
    .get(`https://api.unsplash.com/search/photos?query=${word}&client_id=${import.meta.env.VITE_UNSPLASH_API_KEY}`) // ← キーを書き換え
    .then(res => {
      setPhoto(res.data.results)
    })
  }

// ・・・以下略・・・

GitHub で管理するときは、.gitignore ファイルに .env を追加して Git に含めないようにすれば OK。Vite の環境変数については公式ドキュメントも参照してくださいね。

完成!

一旦ターミナルを Ctrl + C で停止させ、再度 npm run dev で起動して確認しましょう。問題なく動作しているのが確認できます。これでひとまず完成です!

コードの全文はGitHubで公開しているので、うまくいかなかった場合は参考にしてみてください!

白黒画像の検索版も作ってみたよ

Web サイトGitHub

同じ方法で、Unsplash に登録されている白黒画像を検索できるBW Photosも作ってみました。複数ページに対応、レイアウトも見やすく調整しています。デモ版なのでアクセス制限があるのですが、よかったらこちらも見てみてください!