以前、Next.jsの中でFetch APIでいろいろなデータを読み込みました。これをベースにもっとたくさんのデータを読み込んで検索ができるアプリ「ビートルズDB」を作りました。今回、Next.jsの動的ページのルーティング、動的APIのルーティング、APIのデータ整形の設定を経験できました。ビートルズが結成から解散までにリリースした楽曲(1961〜1970年)のデータベースです。同時期にリリースしたバックバンド活動、ソロ活動の作品も掲載しています。それではいきましょう!
【目次】
※参考:前回記事
【React】Realtime Database、Firestore(&スプレッドシート)のデータをFetch APIで読み込む(Next.js環境) - クモのようにコツコツと
※参考:【React】ReactでWebアプリを作るシリーズまとめ qiita.com
作ったもの
トップページでは楽曲情報全370曲が一覧になっている。
カテゴリなどで絞り込むこともできる。
検索窓からフリーワード検索もできる。
楽曲名を押すと楽曲の詳細情報を見ることができる。 (詳細情報上のリンクから楽曲の絞り込みもできる)
作ったアプリがこちら
※参考:ビートルズDB
アプリの使い方
ソースコード
なぜこのアプリを作ったのか
ビートルズ好きとして、自分の中で曖昧になっていることをいつでも調べられるアプリを作りたかった
- ビートルズって曲がたくさんあるけどどんな順番でリリースされたんだっけ?(シングル、アルバム、EP含め)
- 曲によって作曲、ボーカル、演奏パートが異なるのを把握したい
- 録音、ジャケット、映像など裏方の人間関係もわかるようにしたい
- 並行して作られたバックバンド活動やソロ活動の楽曲も含めた情報を把握したい
- それらのデータをいろんな切り口で絞り込みや検索をできるようにしたい
などなど。
内容は英語版Wikipediaのビートルズ関連のページから主に引用した。情報が足りない部分は日本語版Wikipedia、公式サイト、CDライナーノートなどから補った。
※参考:The Beatles discography - Wikipedia
なお、今後も各楽曲情報のページの一番下に備考(イイダリョウによる楽曲解説)をコツコツと追記していく予定ですー
このアプリで経験できたこと
このアプリで下記のことを経験できた。
- API RoutesでAPIルーティング設定でレスポンスを返す
- Dynamic Routesで動的ルーティングでページ生成
- indexOfで絞り込みや検索設定
- 区切り文字をsplit()で分割して配列作成
他にもいろいろなことを経験できた。やったことや参考になった記事など、詳細はこちら。
※参考:ビートルズデータの読み込み、表示を実装 · Issue #3 · ryo-i/beatles-db · GitHub
スプシからJSONデータを取得
前回のこちらの記事の方法でスプレッドシート上につくったデータをJSONデータにした。
※参考:【React】Realtime Database、Firestore(&スプレッドシート)のデータをFetch APIで読み込む(Next.js環境) - クモのようにコツコツと
スプレッドシートの元データ
Google Sheets APIでJSONデータを取得
データは「pages/api/data」フォルダの中に格納した
当初はFirebase上にJSONデータを置いてみたが、認証なしでread OKにするとセキュリティ警告メールが来るため、断念した。
※参考:ビートルズデータの読み込み、表示を実装 · Issue #3 · ryo-i/beatles-db · GitHub
API RoutesでAPIルーティング設定でレスポンスを返す
上記のJSONデータは懸念がいくつかあった
- このままだと使いづらい形式で、ブラウザ側でページ上には不要なデータを読み込み、不要なデータを削除する必要がある
- APIを細かいパスで指定してデータを取得するとデータ量は減るが通信回数が増え、また複数のレスポンスの内容を組み合わせる日強うがある
データ量やデータ通信回数を抑えるにはサーバ側でAPIの設定を行いたい。
※参考:Webプログラミングでは通信量を減らすためにAPIへのリクエストの最適化などが常に行われていますが、動画や画像と比べてたかだか数KBのテキストのやりとりを時間をかけて最適化する意味はあるのでしょうか? - Quora
※参考:Web API 設計入門
API Routes機能によってデータ整形をサーバ上で行い、最適な形にしてレスポンスで返すことができた!
※参考:Next.js の API Routes 機能で Web API を作成する|まくろぐ
※参考:API Routes: はじめに | Next.js
APIの設定ファイル名に[hoge].js
と角カッコをつけると動的なルーティングもできる!
※参考:API Routes: 動的APIルーティング | Next.js
API設定は「/pages/api/beatles」フォルダで行った
※参考:beatles-db/pages/api/beatles at main · ryo-i/beatles-db · GitHub
例えば楽曲情報のファイルは「/track/[track].ts 」に設定している。この[track]
の部分は動的に変更できる。
※参考:beatles-db/[track].ts at main · ryo-i/beatles-db · GitHub
「beatles.json」のJSONデータをbeatlesData
で読み込み
import beatlesData from '../../data/beatles.json';
引数のトラックナンバーをもとにbeatlesData
から該当する楽曲の情報を取得する関数
// Get Track Obj Data const getTrackObjData = (track) => { const keyArray = beatlesData.values[0]; const valArray = beatlesData.values[track]; const thisObj = {}; if (keyArray.length === valArray.length) { for (var i = 0; i < keyArray.length; i++) { thisObj[keyArray[i]] = valArray[i]; } } return thisObj; };
API設定本体
// Response export default (req, res) => { const { query: { track } } = req; // console.log('req.query', req.query); const trackObjData = getTrackObjData(track); // console.log('trackObjData', trackObjData); res.status(200).json(trackObjData); }
- リクエスト
req
:クエリquery
から情報トラックナンバーtrack
を取得 - 変数
trackObjData
:トラックナンバーtrack
をgetTrackObjData()
の引数に入れてデータから楽曲情報を取得 - レスポンス
res
:ステータスコードstatus
の値200
(成功)とJSONデータでtrackObjData
を返す
ここらへんの書き方は以前、Node.jsやExpressで経験した内容と似ていてわかりやすく感じた♪
※参考:Node.js / Expressを習得するためにやったことまとめ(随時更新) - Qiita
Dynamic Routesで動的ルーティングでページ生成
上記のAPIへのリクエストはページ上から行う。このページも動的に生成したい。(フリーワードの検索結果や370曲分の楽曲情報ページを作るのは現実的ではない。
Dynamic Routes機能によって動的なルーティング設定でページ生成ができた!
※参考:Routing: Dynamic Routes | Next.js
APIのルーティングと同様、ファイル名に[hoge].js
と角カッコをつけると動的なルーティングでページ生成ができる!
例えば楽曲情報のページは「/pages/track/[number].tsx」の場合
※参考:beatles-db/[number].tsx at main · ryo-i/beatles-db · GitHub
// Get TrackInfo export async function getStaticProps({ params }) { const number = params.number; const res = await fetch(`https://beatles-db.vercel.app/api/beatles/track/${number}`); const trackInfo = await res.json(); // console.log('number', number); // console.log('trackInfo', trackInfo); return { props: { trackInfo } }; }
- 引数
params
でパスの[number]
から楽曲ナンバーを取得 - Fetch APIで動的ルーティングに
number
を入れて楽曲情報のレスポンスを取得 - レスポンスの楽曲情報を
props. trackInfo
として取得
trackInfo
のid
とtrack
はフックに入れてInnerTrack
コンポーネントにコンテキストとして渡している
// Component const Track = ({ trackInfo }) => { const [trackNumber, setTrackNumber] = useState(trackInfo.id); const [trackName, setTrackName] = useState(trackInfo.track); // 中略 return ( <> // 中略 <main> <h1>楽曲情報</h1> <numberContext.Provider value={{trackNumber, setTrackNumber, trackName, setTrackName}} > <InnerTrack /> </numberContext.Provider> </main> // 中略 </> ); }
フックのコンテキスト設定
※参考:beatles-db/numberContext.ts at main · ryo-i/beatles-db · GitHub
import React, { createContext } from 'react'; export const numberContext = createContext({} as { trackNumber: string; setTrackNumber: React.Dispatch<React.SetStateAction<string>>; trackName: string; setTrackName: React.Dispatch<React.SetStateAction<string>>; });
参考になった記事
※参考:【TypeScript】useContextとuseStateを組み合わせて、子孫コンポーネントから直接先祖コンポーネントのstateを編集する - Qiita
※参考:React Hooks: 子コンポーネントから親の状態をレンダー時に変えたら叱られた ー Warning: Cannot update a component while rendering a different component - Qiita
※参考:コンテクスト – React
indexOfで絞り込みや検索設定
indexOf
を使うことで楽曲一覧ページ上で様々な絞り込みや検索設定を実現できた。
InnerIndexコンポーネント
※参考:beatles-db/InnerIndex.tsx at main · ryo-i/beatles-db · GitHub
useEffect()でAPIへの通信をリクエスト。レスポンスからページ表示に必要な処理を実行
useEffect(() => { // 中略 // fetch const url: string = isCategory ? '../api/beatles' + queryText : 'api/beatles' + queryText; // console.log('url', url); async function getTracksData (url) { try { const res = await fetch(url); const resJson = await res.json(); const data = resJson; // console.log('data', data); setTracksData(data.trackList); setYearList(data.yearList); setFormatList(data.formatList); setPageInfo(data.pageInfo); setIsLoaded(true); } catch(error) { setError(error); console.log('err', error); setIsLoaded(true); } }; // 中略 }, [router, queryParam, categoryName]);
API側の処理
※参考:beatles-db/index.ts at main · ryo-i/beatles-db · GitHub
// Response export default (req, res) => { let resultData = beatlesData.values; const query = req.query; // 中略 // Get Filter Data if (query.year) { resultData = getFilterData(resultData, 'year', query.year, 'exact'); // 中略(他の条件) } // 中略 (データ整形処理) const tracksData = {}; tracksData['pageInfo'] = pageInfo; tracksData['yearList'] = yearsArray; tracksData['formatList'] = formatsArray; tracksData['trackList'] = tracksArray; res.status(200).json(tracksData); }
- まず
resultData
にbeatlesData
の全てのデータを取得 query
にリクエストのクエリ情報を取得- クエリの条件によって
getFilterData()
関数でデータを絞りこむ - 絞り込んだデータを整形した
tracksData
をレスポンスとして返す
getFilterData()関数による絞り込み
※参考:beatles-db/getFilterData.ts at main · ryo-i/beatles-db · GitHub
const getFilterData = (data, key, value, match) => { const isExact = match === 'exact'; const isPartial = match === 'partial'; const isSeach = match === 'search'; const result = data.filter((item, index) => { if (isExact && item[keyNumbers[key]] === value) { return item; } else if (isPartial && item[keyNumbers[key]].indexOf(value) !== -1) { return item; } else if (isSeach) { for (let i = 0; i < item.length; i++) { const isMatch = item[i].toLowerCase().indexOf(value.toLowerCase()) !== -1; if (isMatch) { return item; } } } });
isExact
は完全一致の処理。値value
とkey
列の値が===
で一致したら配列result
に追加isPartial
は部分一致の処理。値value
とkey
列の値がindexOf
で-1
以外であれば配列result
に追加isSeach
はフリーワード検索。値value
とデータ全体がindexOf
で-1
以外であれば配列result
に追加
(toLowerCase()
で値同士を小文字に変換して比較している)
の大文字小文字を区別しない文字列比較の参考
※参考:JavaScript での大文字小文字を区別しない文字列比較 | Delft スタック
フリーワード検索で前後の空白文字の削除は検索窓のコンポーネント側で行った
※参考:beatles-db/Header.tsx at main · ryo-i/beatles-db · GitHub
const getSearchValue = (searchValue) => { let thisValue = searchValue; thisValue = thisValue.replace(/^\s+|\s+$/g, ''); setSearchValue(thisValue); return thisValue; }
replace()
で正規表現を使って削除
※参考:入力された文字列に含まれる空白文字を削除する方法 - JavaScript TIPSふぁくとりー
あと、検索実行をinputタグでエンターでも実行できるようにした。
※参考:beatles-db/Header.tsx at main · ryo-i/beatles-db · GitHub
return ( <form className="search" onSubmit={(e) => e.preventDefault()}> <input type="text" value={searchValue} onChange={(e) => setSearchValue(e.target.value)} onKeyPress={(e) => { if (e.key == 'Enter') { e.preventDefault(); doSearch(); } }} /> <button type="button" onClick={() => doSearch() }>検索</button> </form> );
指定方法はe.key == 'Enter'
で簡単だった。スマホの「改行」でも動いた♪
※参考:ReactでInputフォームのEnterキーで処理を行う | freks blog
区切り文字をsplit()で分割して配列作成
テキストの中の区切り文字で配列に分割するモジュールを作った。
※参考:beatles-db/getDividedArray.ts at main · ryo-i/beatles-db · GitHub
const getDividedArray = (text) => { let resultArray = []; const delimiterSlash = ' / '; const isMultipleSlash = text.indexOf(delimiterSlash) !== -1; if (isMultipleSlash) { resultArray = text.split(delimiterSlash).map((item) => { return item; }); } else { resultArray.push(text); } return resultArray; }; export { getDividedArray };
- 引数のtextの中に区切り文字
' / '
があるかをチェック - もし区切り文字
' / '
があればtext
をsplit()
で区切り文字で分割し、配列に入れる - なければ
text
そのものを配列に入れる
※参考:String.prototype.split() - JavaScript | MDN
たとえばこういう連想配列があったとして
{ source: "https://en.wikipedia.org/wiki/My_Bonnie_Lies_over_the_Ocean / https://en.wikipedia.org/wiki/My_Bonnie" };
この値の中の/
を区切り文字にして分割した配列を作成する。
[ 'https://en.wikipedia.org/wiki/My_Bonnie_Lies_over_the_Ocean', 'https://en.wikipedia.org/wiki/My_Bonnie' ];
取得した配列をmap()
でjsxタグに入れて表示する
const sourceArray = getDividedArray(props.source); return ( <ul> {sourceArray.map((data, index) => <li key={index}><a href={data} target="_blank">{data}</a></li> )} </ul> );
また、こちらを参考に複数の区切り文字で多次元配列を作ることもできた!
つくったモジュール
※参考:beatles-db/getPeopleArray.ts at main · ryo-i/beatles-db · GitHub
その他
その他にも作っている過程でいろいろなエラーがあって、Reactの仮想DOM生成とその他の処理の実行タイミングによるエラーが多かったように思う。ここらへんのライフサイクルの理解を深めたい。
Styled-componentsでもレンダリングのたびにclass名が変わるエラーが起こった(ローカル環境のみ)。いろいろな方法を試したが、最終的にこちらのフラグをつけて実行する方法で解決できた。
※参考:Warning: Prop `className` did not match.Server: xxx の対処
その他、いろいろなつまづきの度にたくさんの記事に助けられました。
※参考:ビートルズデータの読み込み、表示を実装 · Issue #3 · ryo-i/beatles-db · GitHub
最後に
ということで今回は動的なAPI設定、動的なページ設定、API設定でデータ整形などを経験できました。これらの機能を使えば大量のデータを元にした大量のページのアプリを作れることができそうです。
今回のようにデータを読み込むだけであれば関してはNext.jsで完結できそうです。ここからさらにデータを書き込み、修正、削除までを行う場合は、やはりバックエンド側にちゃんとしたデータベースを作る必要はありそうです。その際にはセキュリティ的に認証設定を行う必要性も感じます。
ただ、一つのブラウザで完結させるだけであれば、localstrageを連携すれば簡易的な書き込み、修正、削除は実現できそうな気はします。
今後ですが、同じくデータ系で戦国時代の年表データベースを作ってみたい。あと、最近またクリエイティブ系のコーディングに興味が寄っていっているので、3D、アニメーション、音楽などを組み合わせた作品も作っていきたいです。Next.js環境でAPIで通信した内容を元に動的なコンテンツが作るところまで目指していきたい。
それではまた!
※参考:【React】ReactでWebアプリを作るシリーズまとめ qiita.com