📷

React ✖️ Unsplash APIで画像検索アプリを作る

に公開

はじめに

今回は、APIを叩く練習として、UnsplashというサイトのAPIを使った簡単な画像検索アプリを作ってみます!

完成品はこんな感じです。キーワードを入れて検索すると、関連する画像が取得されます。

githubのソースコードも適宜参照してください。
https://github.com/Ashunar0/unsplash-clone-app

Unsplashとは

Unsplash は、世界中のフォトグラファーが提供する 高品質な写真を無料でダウンロード&利用できるサイト。無料で高画質な画像をサクッと手に入れられる!

しかも、APIを使えばプログラムで画像を検索・取得できるんです!

https://unsplash.com/ja

プロジェクトの作成

まずは任意のディレクトリでプロジェクトを作成しましょう。今回はviteを使いました。

npm create vite@latest unsplash-clone-app
cd unsplash-clone-app
npm install
code .

作成できたら、必要ない部分を消します。App.css``index.cssの中身は全て削除しちゃいましょう。

App.cssは、下のように書いておいてください。

App.css
body{
    text-align: center;
}

App.jsxを、簡単に書き換えておきます。

App.jsx
const App = () => {
  return (
    <div>
      <h1>Hello</h1>
    </div>
  );
};

export default App;

サーバーを立ち上げて、「Hello」とだけ表示されていれば成功です!

npm run dev

APIキーの取得

Unsplash API を利用するために、開発者アカウントの登録をしましょう!

1. Unsplash Developers でキー取得

下記リンクから飛んでください

https://unsplash.com/developers

「Your App」を選択。

新しいアプリを作成するため、「New Applicarion」を選択します。

API の利用ガイドラインを確認したら、全てチェックして、「Accept terms」ボタンをクリック!
(画像では「Decline」が囲まれてますが、その右のボタンです)

アプリの名前と説明を入れてあげます。名前はわかりやすく「image-search-app」などにしておくといいでしょう。これで管理画面に遷移するはずです!

下の方に行くと、↑のように「Access Key」が確認できると思います。これをコピーしましょう!
開発時には、このキーを利用して実装していきます。

2. envファイルにキーを格納

vscodeに戻って、.envファイルを作成します。

今回のようなAPIキーは機密情報なので、基本的に非公開にしておきたいです。そこで、.envファイルに変数として記述しておいて、使う時にそこから呼び出す、という形式にします。

vite環境下では、変数名はVITE_から始めるというルールがあります。ここでは、以下のように書きました。

.env
VITE_UNSPLASH_ACCESS_KEY=your_access_key

your_access_keyの部分を消して、先ほどコピーしたキーを貼り付けてあげてください。
これで、APIを使う準備ができました!

タイトルとフォームを作る

準備は全て完了したので、いよいよアプリの中身を実装していきましょう!

まずは、App.jsxに、タイトルを記述します。

App.jsx
const App = () => {
  return (
    <div>
      <h1>Unsplash Clone App</h1> /* 変更 */
    </div>
  );
};

export default App;

次に、検索するフォーム部分を作ります。ここはコンポーネントとして切り出すことにしました。

srcディレクトリの中に、componentsというディレクトリを新たに作ります。
この中に、Form.jsxというファイルを作成しましょう。

作ったファイルには、以下のように記述します。

Form.jx
const Form = () => {
  return (
    <div className="form-container">
      <input
        type="text"
        name="keyword"
        placeholder="写真とイラストを検索"
        className="search-input"
      />
      <button type="submit" className="search-button">
        検索
      </button>
    </div>
  );
};

export default Form;

App.jsxに戻って、今作ったコンポーネントをインポートしましょう

App.jsx
import Form from "./components/Form"; /* 追加 */

const App = () => {
  return (
    <div>
      <h1>Unsplash Clone App</h1> 
      <Form /> /* 追加 */
    </div>
  );
};

export default App;

これで確認します。

タイトルとフォームの見た目はこれで完成です!しかし、この状態で入力しても、何も起こりません...検索ワードを管理する状態変数を用意して、入力欄で監視しましょう。

App.jsxで状態変数を定義します。

App.jsx
import React, { useState } from "react"; /* 追加 */
import Form from "./Form";

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

  return (
    <div>
      <h1>Unsplash Clone App</h1>
      <Form />
    </div>
  );
};

export default App;

続いて、いま定義したsetWordを、Formコンポーネントに渡し、入力欄で監視できるように設定します。

App.jsx
import React, { useState } from "react";
import Form from "./Form";

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

  return (
    <div>
      <h1>Unsplash Clone App</h1>
      <Form setWord={setWord} /> /* setWordを渡す */
    </div>
  );
};

export default App;
Form.jsx
const Form = ({ setWord }) => {  /* setWordを受けとる */
  return (
    <div className="form-container">
      <input
        type="text"
        name="keyword"
        placeholder="写真とイラストを検索"
        onChange={(e) => setWord(e.target.value)}  /* 入力内容の変化を監視 */
        className="search-input"
      />
      <button type="submit" className="search-button">
        検索
      </button>
    </div>
  );
};

export default Form;

きちんと設定できたのか、試しにwordを表示できるようにしてテストしましょう

App.jsx
import React, { useState } from "react";
import Form from "./Form";

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

  return (
    <div>
      <h1>Unsplash Clone App</h1>
      <Form setWord={setWord} />
      <div>{word}</div> /* 試しに追加 */
    </div>
  );
};

export default App;

これで確認してみます。入力欄に文字を打つと、その下に入力内容が反映されていればokです!
入力欄の変化が監視できるようになりました。

ちなみに、今追加したdivタグは確認用なので、消してもらって大丈夫です。

APIからデータを取得する

それでは、いよいよAPIから画像データを取得していきましょう!このアプリのキモの部分です!

App.jsxに、APIの非同期処理を追加しましょう。ここはほぼ雛形のようなものなので、丸々覚えちゃいましょう!

App.jsx
import React, { useState } from "react";
import Form from "./Form";

const App = () => {
  const [word, setWord] = useState("");
  const accessKey = import.meta.env.VITE_UNSPLASH_ACCESS_KEY; /* 追加 */

  //APIからデータを取得する関数
  const fetchImage = () => {
    fetch(
      `https://api.unsplash.com/search/photos?query=${word}&per_page=30&client_id=${accessKey}`
    )
      .then((res) => {
        return res.json();
      })
      .then((data) => {
        const results = data.results;
        console.log(results);
      });
  };

  return (
    <div>
      <h1>Unsplash Clone App</h1>
      <Form setWord={setWord} />
      <div>{word}</div>
    </div>
  );
};

export default App;

この部分は軽く解説します。
.envに格納したアクセスキーは、上で呼び出しておきます。

const accessKey = import.meta.env.VITE_UNSPLASH_ACCESS_KEY;

fetch関数をつかって、APIから画像のデータを取得します。{wors}に検索ワード、{accessKey}にアクセスキーが格納されます。

fetch(
      `https://api.unsplash.com/search/photos?query=${word}&client_id=${accessKey}`
    )

.thenと繋げることで、fetch関数の処理が終わった後に行う処理を指定できます。これが「非同期処理」です。
ここでは、fetchで取得したデータをdataという変数に格納し、これをjson形式に変換します。

.then((res) => {
    return res.json();
})

このjson形式のデータにはいろんなデータが含まれているので、さらにthenと繋げて、json形式のデータの中から画像のデータだけを取り出します。

.then((data) => {
   const results = data.results;
   console.log(results);
});

あとは、検索ボタンを押した時に画像データが取得されるようにしていきましょう!
App.jsxに、ボタンを押した時の処理を記述します。
その関数を、Formコンポーネントに渡して、ボタンから呼び出せるようにします。

App.jsx
import React, { useState } from "react";
import Form from "./Form";

const App = () => {
  const [word, setWord] = useState("");
  const accessKey = import.meta.env.VITE_UNSPLASH_ACCESS_KEY;

  //APIからデータを取得する関数
  const fetchImage = () => {
    fetch(
      `https://api.unsplash.com/search/photos?query=${word}&per_page=30&client_id=${accessKey}`
    )
      .then((res) => {
        return res.json();
      })
      .then((data) => {
        const results = data.results;
        console.log(results);
      });
  };

  //追加
  //フォームの送信を処理する関数
  const handleSubmit = (e) => {
    if (!word) return; // wordが空の場合は何もしない

    e.preventDefault();
    fetchImage();
  };

  return (
    <div>
      <h1>Unsplash Clone App</h1>
      <Form setWord={setWord} onSubmit={handleSubmit}/> /* 追加 */
      <div>{word}</div>
    </div>
  );
};

export default App;
From.jsx
const Form = ({ setWord, onSubmit }) => { /* 受け取る */
  return (
    <div className="form-container">
      <input
        type="text"
        name="keyword"
        placeholder="写真とイラストを検索"
        onChange={(e) => setWord(e.target.value)}
        className="search-input"
      />
      <button type="submit" onClick={onSubmit} className="search-button"> /* 追加 */
        検索
      </button>
    </div>
  );
};

export default Form;

これで確認してみましょう。入力欄になにかワードを入力して検索します。
consoleを開いて検索ボタンを押してみると、画像データが取得されているのが確認できると思います。

画像を表示する

それでは、取得した画像データから、実際に画像を表示させていきましょう!

App.jsxの中で、画像を管理する状態変数を定義して、画像データを格納します。

App.jsx
import React, { useState } from "react";
import Form from "./Form";
import Images from "./Images"; /* 追加 */

const App = () => {
  const [word, setWord] = useState("");
  const accessKey = import.meta.env.VITE_UNSPLASH_ACCESS_KEY;
  const [images, setImages] = useState([]); /* 追加 */

  //APIからデータを取得する関数
  const fetchImage = () => {
    fetch(
      `https://api.unsplash.com/search/photos?query=${word}&per_page=30&client_id=${accessKey}`
    )
      .then((res) => {
        return res.json();
      })
      .then((data) => {
        const results = data.results;
        console.log(results); /* 消してもok */

        setImages(results);  /* 追加 */
      });
  };

  //フォームの送信を処理する関数
  const handleSubmit = (e) => {
    if (!word) return; // wordが空の場合は何もしない

    e.preventDefault(); // フォームのデフォルトの送信を防ぐ
    fetchImage();
  };

  return (
    <div>
      <h1>Unsplash Clone App</h1>
      <Form setWord={setWord} onSubmit={handleSubmit} />
      <Images images={images} /> /* 追加 */
    </div>
  );
};

export default App;

componentsフォルダの中にImages.jsxを作り、以下のように書きます。

Images.jsx
const Images = ({ images }) => {
  return (
    <div className="image-container">
      {photos.map((image, index) => (
        <div key={index} className="image">
          <img src={image.urls.small} alt="" />
        </div>
      ))}
    </div>
  );
};

export default Images;

ここでは、App.jsxから画像データの入ったimagesを受け取り、map関数を使って展開しています。
これで、検索ボタンを押したら画像が表示されるはずです!

CSSを適用しよう

あとは、CSSをお好みで付けてあげてください!今回はこんな感じにしました。

App.css
body {
  text-align: center;
}

.form-container {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 10px;
  width: 100%;
  max-width: 500px;
  margin: 20px auto;
  padding: 10px;
  background: #ffffff;
  border-radius: 12px;
  box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
}

.search-input {
  flex: 1;
  padding: 12px;
  font-size: 16px;
  border: 2px solid #ddd;
  border-radius: 8px;
  outline: none;
  transition: all 0.3s ease;
}

.search-input:focus {
  border-color: #007bff;
  box-shadow: 0px 0px 8px rgba(0, 123, 255, 0.3);
}

.search-button {
  padding: 12px 20px;
  font-size: 16px;
  font-weight: bold;
  color: white;
  background: linear-gradient(45deg, #007bff, #0056b3);
  border: none;
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.3s ease;
}

.search-button:hover {
  background: linear-gradient(45deg, #0056b3, #003d80);
  transform: scale(1.05);
}

.image {
  display: flex;
  align-items: center;
}

img {
  width: 100%;
}

.image-container {
  margin: 0 auto;
  width: 80%;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 10px;
}

これにて完成です!!

Discussion