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

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


↓私が10年以上利用している会計ソフト!

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

目次

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も作ってみました。複数ページに対応、レイアウトも見やすく調整しています。デモ版なのでアクセス制限があるのですが、よかったらこちらも見てみてください!

シェアする

ニュースレター

Web制作の最新情報やWebクリエイターボックスからのお知らせ、中の人の近況等を定期的にお送りいたします。 ぜひご登録ください!もちろん無料です! :)