React + Unsplash APIで画像検索アプリを作ろう
Reactの勉強がてら、高画質な画像を配布しているUnsplashが提供しているUnsplash APIを使って画像検索アプリを作ってみました。その復習に作成手順をまとめてみたので、これからReactを勉強しよう!と思っている方の役に立てれば幸いです!
↑私が10年以上利用している会計ソフト!
この記事は動画でも解説しています。動画派の方はぜひご覧ください!
目次
- Unsplashとは
- こんなアプリを作ってみよう
- 1. Unsplash APIの開発者登録
- 2. ViteでReactアプリのベースを作成
- 3. タイトル部分の作成(Title.jsx)
- 4. 検索フォームの作成(Form.jsx)
- 5. Unsplashのデータを取得
- 6. 検索結果の表示(Results.jsx)
- 7. 環境変数の作成(.env)
- 完成!
- 白黒画像の検索版も作ってみたよ
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」というファイルを新規作成します。Title
や Form
と同様、雛形となる部分を記述しましょう。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で公開しているので、うまくいかなかった場合は参考にしてみてください!
白黒画像の検索版も作ってみたよ
同じ方法で、Unsplashに登録されている白黒画像を検索できるBW Photosも作ってみました。複数ページに対応、レイアウトも見やすく調整しています。デモ版なのでアクセス制限があるのですが、よかったらこちらも見てみてください!