- 投稿日:2021-02-25T17:17:34+09:00
Spotify API + Next.jsで作る選曲支援Webアプリ
概要
Spotify APIの楽曲レコメンド機能を使って、DJ Mix用のプレイリストを作成できるWebアプリケーションを開発しました。まずはそのアプリケーションの内容をご紹介します。
実装はTypeScript + Next.jsで行いVercelにデプロイする形にしたところ、非常に高い開発体験が得られました。後半はSpotify APIの使い方を含めて、Next.jsでの実装やVercel上での設定について書いていきます。
できたもの
AUTOMISCE - Automate Your Mix with Spotify API
使い方
まず"Sign in with Spotify"ボタンでSpotifyにログインします。次に右側の検索欄で最初の曲を選んでプレイリストに追加します。するとその曲と似たテンポでかつテンション感が少し上の曲が"Upper Tracks"欄に、少し下の曲が"Downer Tracks"欄に表示されます。
そこから次々と曲を選んでいくとプレイリストができるので、あとは名前を付けて保存すればSpotifyのアプリなどから聴くことができます。またWebアプリ内で楽曲を再生することもできます。1
想定しているのはDJがミックス用の曲を選曲しているシーンです。曲同士をシームレスにつなぎ合わせるために、テンポとdanceability(踊りやすさ)が似た曲をレコメンドしています。またミックスの中でダンスフロアを盛り上げたり落ち着けたりするのもDJの仕事なので、選んだ曲よりテンション感が少し高いものと低いものを分けてレコメンドしました。
プロジェクト構成
- 言語: TypeScript
- フレームワーク: Next.js
- インフラ: Vercel (+ AWS Route 53)
リポジトリ: yuki-oshima-revenant/spotify-mix-automation
型が付いていないと禁断症状に陥るのでTypeScriptにしました。またフレームワークは最近業務でも使い始めたNext.jsを採用しています。今回のようにログイン処理などでちょっとしたAPI呼び出し用のバックエンドが必要な場合、Next.jsのAPI Routeはベストプラクティスに近い便利さがあると感じました。
DBなどが必要ない純粋なフロントエンドのみの構成なのでインフラはVercelで完結します。GitHubにプッシュしておいてVercel画面上で連携させるだけでデプロイ/ビルド/配信までが簡単に終わるのでとてつもなく良い開発体験が得られました。
ただし、ドメインをAWS Route53で取得していたのでDNSだけRoute53が関わりました。VercelとRoute53の連携も非常に簡単に行えました。
実装2
Spotifyへのログイン
Spotify APIを利用する際は、まずSpotify for Developersにサインインします。その上でアプリケーションの設定を行います。こちらのヘルプ通りに進めました。
アプリケーションが作成できたら、ダッシュボードからそのアプリを選択して
EDIT SETTING
ボタンから各種設定を行います。認証後にリダイレクトされてくるURIをあらかじめ設定しておく必要があります。ローカルでの開発段階ではhttp://localhost:3000/api/auth/authorize
のようにlocalhost
を設定しておきましょう。実際のドメインで動作させる際はhttps://automisce.unronritaro.net/api/auth/authorize
といったURIを追加します。また同じくアプリケーションの詳細画面に表示される
Client ID
とClient Secret
が認証処理に必要になります。(Client Secret
はSHOW CLIENT SECRET
をクリックすると表示)ユーザー認証は以下の手順で行います。
- ユーザーをアプリケーションから
https://accounts.spotify.com/authorize
にリダイレクトさせる- ユーザーが認証画面で権限を許可する
- 設定したリダイレクトURIに
code
というパラメータ付きでリダイレクトされるcode
、およびClient ID
、Client Secret
を付けてhttps://accounts.spotify.com/api/token
にリクエストを送る- アクセストークンが得られる
そのあとはアクセストークンを使って各APIからデータを取得することができます。基本的にAuthorization Guide通りの実装です。
今回のアプリケーションでは
Sign in with Spotify
ボタンを押すとリダイレクトするようにしました。その際にClient ID
をパラメータにつけて送る必要があります。この値を環境変数から読み込むことにしました。Next.jsは特に設定せずとも
.env.local
という名前の付いたファイルにCLIENT_ID = 'xxxxx'
と書いて、プロジェクトルートディレクトリに置けばそれらを環境変数として読み込んでくれます。ただしそれはgetStaticPropsなどのdata fetching methods
やAPI Routeでのみ有効です。By default all environment variables loaded through .env.local are only available in the Node.js environment, meaning they won't be exposed to the browser.
https://nextjs.org/docs/basic-features/environment-variables
今回は
getStaticProps
のなかでリダイレクト用のURLを作ってそれをコンポーネントに渡すようにしました。// pages/index.tsx const Index = ({ loginPath }: InferGetStaticPropsType<typeof getStaticProps>) => { const login = useCallback(() => { window.location.href = loginPath; }, [loginPath]); ... return ( <button onClick={login}> Sign in with Spotify </button> ) }); export const getStaticProps: GetStaticProps = async () => { // https://accounts.spotify.com/authorizeへのリクエストパラメータに必要な項目を設定 const scopes = ['streaming', 'user-read-email', 'user-read-private', 'playlist-modify-public', 'playlist-modify-private']; const params = new URLSearchParams(); params.append('client_id', process.env.CLIENT_ID || ''); params.append('response_type', 'code'); params.append('redirect_uri', process.env.RETURN_TO || ''); params.append('scope', scopes.join(' ')); params.append('state', 'state'); return { props: { loginPath: `https://accounts.spotify.com/authorize?${params.toString()}` } } }; export default Index;このリダイレクト先ページでユーザーが権限を許可すると、
redirect_uri
に指定したURL(http://localhost:3000/api/auth/authorize
など)に?code=xxxxxxx&state=state
というパラメータが付いたGETリクエストが行われます。これをAPI Routeで受け取って、tokenの取得処理を行いましょう。
// pages/api/auth/authorize.ts type SpotifyAuthApiResponse = { access_token: string, token_type: string, scope: string, expires_in: number, refresh_token: string } const authorize = async (req, res) => { const { code, state } = req.query; const params = new URLSearchParams(); params.append('grant_type', 'authorization_code'); params.append('code', code as string); params.append('redirect_uri', process.env.RETURN_TO as string); const response = await axios.post<SpotifyAuthApiResponse>( 'https://accounts.spotify.com/api/token', params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': `Basic ${Buffer.from(`${process.env.CLIENT_ID}:${process.env.CLIENT_SECRET}`, 'utf-8').toString('base64')}` } } ); req.session.set('user', { accessToken: response.data.access_token, }); res.status(200).redirect('/'); }; export default withSession(authorize);URLパラメータから
code
を取り出して、https://accounts.spotify.com/api/token
にリクエストする際のパラメータに入れます。またトークン取得用のAPIはAuthorization
ヘッダーにClient ID
とClient Secret
から作成した文字列をbase64でエンコードして送る必要があります。ここで
process.env.CLIENT_SECRET
などはNext.jsのAPI Route、つまりサーバ側でのみアクセス可能です。API Routeはこうしてフロントエンド側に露出したくないコードを単一のプロジェクト内に記述する際に役立ちます。言い換えると、API Routeによってアプリケーションの一部の処理をユーザーから隠蔽することができるわけです。もしこれがなければ、Spotify APIのヘルプにあるようにExpressなどでwebサーバを立ち上げる必要があるでしょう。Next.jsのAPI Routeによってその手間が不要になっています。こうして得たSpotify APIのアクセストークンはセッション(Cookie)に保存しておきます。Next.jsでセッションを使うライブラリはいくつかありますが、今回はシンプルにCookieでセッションを貼ることができればいいのでnext-iron-sessionを利用しました。
最後にトップページにリダイレクトさせれば認証処理は完了です。
Spotify APIの利用
あとはアクセストークンを使ってSpotify APIにアクセスしていきます。たとえば楽曲のレコメンドを行うAPI Routeは以下のようにSpotify APIを呼び出しています。3
// pages/api/track/recommend.ts type RecommendType = 'upper' | 'downer'; type AudioFeature = { danceability: number, energy: number, id: string, instrumentalness: number, key: number, liveness: number, loudness: number, mode: number, tempo: number, valence: number, track_href: string }; type SpotifyRecommendApiResponse = { tracks: TrackItem[] }; type TrackItem = { album: { href: string, name: string, images: { url: string, height: number }[] }, artists: { href: string, name: string }[], href: string, id: string, name: string, uri: string, duration_ms:number, }; type SpotifyFeaturesApiResponse = { audio_features: AudioFeature[] }; const getRecommendTracks = async (audioFeature: AudioFeature, accessToken: string, type: RecommendType) => { // レコメンド対象の曲と似たdanceabilityの曲が対象 const minDanceability = audioFeature.danceability * 0.8; const maxDanceability = audioFeature.danceability * 1.2; // レコメンドのタイプが'upper'なら元の曲からenergyの少し高い曲が、'downer'なら少し低い曲が対象 const minEnergy = type === 'upper' ? audioFeature.energy : audioFeature.energy * 0.8; const maxEnergy = type === 'upper' ? audioFeature.energy * 1.2 : audioFeature.energy; const recommendationsParams = new URLSearchParams(); recommendationsParams.set('seed_tracks', audioFeature.id); // DJが曲同士をつなぎ合わせやすいように似たテンポの曲が対象 recommendationsParams.set('min_tempo', (audioFeature.tempo * 0.9).toString()); recommendationsParams.set('max_tempo', (audioFeature.tempo * 1.1).toString()); recommendationsParams.set('min_danceability', (minDanceability).toString()); recommendationsParams.set('max_danceability', (maxDanceability).toString()); recommendationsParams.set('min_energy', (minEnergy).toString()); recommendationsParams.set('max_energy', (maxEnergy).toString()); const recommendationsResponse = await axios.get<SpotifyRecommendApiResponse>( `https://api.spotify.com/v1/recommendations?${recommendationsParams.toString()}`, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': `Bearer ${accessToken}` } } ); // recommendations APIで取得した楽曲の特徴情報を取得 const featuresParams = new URLSearchParams(); featuresParams.append('ids', recommendationsResponse.data.tracks.map((item => item.id)).join(',')); const featuresResponse = await axios.get<SpotifyFeaturesApiResponse>( `https://api.spotify.com/v1/audio-features?${featuresParams.toString()}`, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': `Bearer ${accessToken}` } } ); const audioFeatures = featuresResponse.data.audio_features; return recommendationsResponse.data.tracks.map((item) => { const targetItemFeature = audioFeatures.find((feature) => (feature.id === item.id)); return { ...item, audioFeatures: targetItemFeature }; }); }; const recommendedTracks = async (req, res) => { // このAPIへのリクエストからレコメンドする元の曲の特徴情報を受け取る const audioFeature = req.body.track.audioFeatures; // セッションからアクセストークンを取り出す const accessToken = req.session.get('user').accessToken; const [upperTracks, downerTracks] = await Promise.all( [ getRecommendTracks(audioFeature, accessToken, 'upper'), getRecommendTracks(audioFeature, accessToken, 'downer') ] ); res.status(200) res.json({ upperTracks, downerTracks }); } }; export default withSession(recommendedTracks);基本的に、Spotify APIを呼び出す際は
Authorization
ヘッダーにBearer ${accessToken}
というようにアクセストークンを含めて送ります。このAPI Routeはユーザーが検索してプレイリストに追加した楽曲のメタ情報をもとにSpotify APIがレコメンドした曲を取得して返しています。レコメンド元となる曲と
- danceabilityの値が近い
- energyの値が元の曲より少し高いか低い
- tempoの値が近い
ことを条件としてSpotify APIの楽曲レコメンドAPIを呼び出します。
seed_tracks
パラメータにレコメンド元の曲のidを入れておくだけでなく、こうしたパラメータを指定することで目的の曲に絞り込むことができるわけです。Web Playback SDK
ブラウザ上でのSpotifyの楽曲再生にはWeb Playback SDKを利用しました。Reactでの実装はこちらのライブラリのソースコードが参考になります。use-spotify-web-playback-sdk
SDK自体はCDNから読み込む必要がありますが、型情報は@types/spotify-web-playback-sdkがあるのでnpmでインストールすればOKです。
npm install --save-dev @types/spotify-web-playback-sdk自分のアプリケーションでは以下のように実装しました。
// pages/index.tsx // ログイン情報を取得するAPI用のフック // Web Playback SDKで使うため、セッションからアクセストークンを取得している const { data: loginData, error: loginError, mutate: loginMutate } = useLoginApi(); // refにプレイヤーのインスタンスを格納しておく const playerRef = useRef<Spotify.SpotifyPlayer | null>(null); const [deviceId, setDeviceId] = useState<string>(); useEffect(() => { if (loginData && loginData.accessToken) { // window.onSpotifyWebPlaybackSDKReadyのコールバックを定義する // SDKが読み込まれたタイミングでこのコールバックが実行される window.onSpotifyWebPlaybackSDKReady = () => { const player = new Spotify.Player({ name: 'AUTOMISCE Player', getOAuthToken: async (cb) => { cb(loginData.accessToken as string); }, volume: 0.5 }); player.addListener('ready', ({ device_id }) => { // ここで楽曲を再生する際に必要なdevice_idを取得してstateに格納しておく setDeviceId(device_id); }); player.connect(); playerRef.current = player; }; if (!window.Spotify) { // Web Playback SDKを読み込む const scriptTag = document.createElement('script'); scriptTag.src = 'https://sdk.scdn.co/spotify-player.js'; document.head!.appendChild(scriptTag); } } }, [loginData]); ...あとは
https://api.spotify.com/v1/me/player/play
APIにdevice_id
と楽曲のuri
(recommendations APIなどから取得可能)を送るとブラウザ上で再生が始まります。一時停止などは
Spotify.SpotifyPlayer
インスタンスのtogglePlay
メソッドを使いましょう。レコメンドされて出てきた曲の再生ボタンは以下のようになっています。
// lib/component/TrackCard // trackやsetState系は親コンポーネントから引数で取得 <AiFillPlayCircle className={styles.button} onClick={() => { // スマートフォンではSDKが動作しないのでopen.spotify.comのリンクを開く const userAgent = window.navigator.userAgent.toLowerCase(); if (userAgent.indexOf('iphone') != -1 || userAgent.indexOf('ipad') != -1 || userAgent.indexOf('android') != -1) { window.open(`https://open.spotify.com/track/${track.id}`) return; } if (playingTrack?.id === track.id) { playerRef.current?.togglePlay(); } else { playerRef.current?.pause(); setPlayingTrack(track); try { // '/api/track/play'で'https://api.spotify.com/v1/me/player/play'にパラメータを送る axios.post('/api/track/play', { deviceId, uris: [track.uri] }); setIsPlaying(true); } catch (e) { setPlayingTrack(); } } }} />曲のプレイヤーにあるシークバーは少し工夫が必要でした。こちらの記事を参考に実装しています。audioプレイヤーをシークバーで操作する - ゆったりWeb手帳
100ミリ秒ごと(適当)に
Spotify.SpotifyPlayer
インスタンスのgetCurrentState
から再生位置を取得して、stateに保持しておきます。その値に従ってcssスタイルのbackground-size
を変更することで、背景色によって再生位置を表示することができるようになります。またシークバーの任意の場所をクリックされたときに、その位置の横幅全体から見た割合を計算してその位置に曲の再生位置を変更します。再生位置の変更はSpotify.SpotifyPlayer
インスタンスのseek
メソッドを使います。// pages/index.tsx const [playingTrackPosition, setPlayingTrackPosition] = useState<number>(0); const seekbarRef = useRef<HTMLDivElement | null>(null); useEffect(() => { if (isPlaying) { const timeout = setInterval(async () => { const palyerState = await playerRef.current?.getCurrentState(); setPlayingTrackPosition(palyerState?.position || 0); }, 100); return () => { clearInterval(timeout) }; } }, [isPlaying]); return ( ... <div className={styles.seekbar} ref={seekbarRef} style={{ backgroundSize: `${(playingTrackPosition / playingTrack.duration_ms) * 100}%` }} onClick={(e) => { const mouse = e.pageX; const rect = seekbarRef.current?.getBoundingClientRect(); if (rect) { const position = rect.left + window.pageXOffset; const offset = mouse - position; const width = rect?.right - rect?.left; playerRef.current?.seek(Math.round(playingTrack.duration_ms * (offset / width))) } }} /> ... );デプロイ
Vercelへのデプロイ自体はログインしてGitHubのリポジトリを連携するだけで終わり非常に快適です。ここではドメインと環境変数の設定について書いていきます。
ドメイン
自分はドメイン(
unronritaro.net
)をAWS Route53で保有しているので、そのサブドメインをVercelにルーティングしようと思いました。その手順はこの記事の通りです。VercelでホスティングしているサイトにRoute53で取得したドメインをサブドメインとして設定する | DevelopersIOVercel上ではGitHubと連携したプロジェクトの「Settings」>「Domains」画面で以下のように設定します。
automisce.unronritaro.net
が追加したドメインです。Route 53側は以下のように設定しています。「ホストゾーン」からレコードを作成してレコードタイプはCNAME、ルーティング先は
cname.vercel-dns.com
を指定すればOKです。Vercelは外部のドメインからのルーティングも簡単に設定できるよう設計されていると感じました。
環境変数
ローカルサーバで開発している間は
Client ID
とClient Secret
を.env.localファイルに書いて読み込んでいましたが、このファイルをGitHubのリポジトリに含めるわけにはいきません。これらの値はVercelの環境変数として設定します。プロジェクトの「Settings」>「Environment Variables」画面から設定します。
- typeは「Secret」を選択
- nameは
client_id
など任意に設定- valueは「Create new Secret for…」を選択して表示されるモーダルに入力
- 適用する環境は「Production」「Preview」が選ばれたデフォルトのままでOK
注意点として、画面上から設定した環境変数を適用するためにはもう一度ビルドする必要があります。「Deployments」画面から最新のデプロイのメニューを開き、「Redeploy」を選べば再ビルドされます。
まとめ
- できたもの: AUTOMISCE - Automate Your Mix with Spotify API
- Next.jsのAPI RouteはSpotify APIなど共通鍵を使うAPI呼び出しに使うとGood
- Vercelでデプロイ、ドメイン/環境変数設定もストレスレス
PCのブラウザで開いた場合のみ楽曲の再生が可能です。スマートフォンのブラウザでは後述するWeb Playback SDKが対応していないため、再生ボタンを押すとSpotifyのアプリが開く仕様にしました。 ↩
本文中に示したコードは簡単のため一部省略しています。完全な実装はリポジトリのソースコードを参照してください。 ↩
Spotify APIの詳しい仕様: Get Recommendations / Get Audio Features for Several Tracks ↩
- 投稿日:2021-02-25T11:36:51+09:00
GraphQLの採用についての考察
はじめに
技術調査として前から気になっていたGraphQLを調査したので今後、新規プロジェクトにおいて採用できるかの考察をまとめておきます。
※ 今回は、REST APIと比較してGraphQLの利点と使いどろこをまとめています検証環境
- OS: macOS
- python 3.9
- Web Framework: FastAPI
- Front: React.js & Material UI
GraphQLとは
GraphQL | A query language for your API
Facebookが開発元のオープンソースでREST APIと同じく特定エンドポイントにリクエストをしてレスポンスを受け取る仕組みにかわりはないのですが以下の点が違います・Rest API
同一エンドポイントに対してデータ取得・登録・更新などの処理をGET・POST・PATCHなどのHTTPメソッドの使いわけによって行う。
また以下のユーザー情報に関してのリソースに対しては/user
というパスになっているが、
これが、ECサイトの様にカートに対してのデータにアクセスする場合は/cart
みたいにエンドポイントが増えていく。curl -x GET http://localhost:3000/user/{id} (ユーザー情報取得) curl -x POST -d '{ "uesr_name": "yamada", "first_anme": "Hanako", "last_name": "Yamada"} http://localhost:3000/user (ユーザー登録) curl -x PATCH -d '{ id:3, "last_name": "Tanaka"} http://localhost:3000/user (ユーザー情報更新)
・GraphQL
全てのリソース(ユーザー情報・カートデータ)に対して同一エンドにリクエストを行う。
また、取得・登録・更新などの処理はオペレーション型と言われる、データ取得系のquery、データ更新系のmutation、サーバーサイドのイベントを受け取るsubscriptionをリクエストのボディに定義して行うEndpoint: http://localhost:3000/graphql # Full filed query { getUser(id: 1) { id userName firstName lastName } } query { getUser(id: 1) { userName firstName lastName } } # Create & update mutation createUser { createUser(newUser: { userName: "h-tanaka", firstName: "hanako", lastName: "Tanaka", }) { id userName firstName lastName } }FastAPIで実際に試してみる
インストールからコーディングまでの手順は省略させて頂きます
上記に関してはこちらの記事を参考にして頂ければと思います。
安全なGraphQL API開発が出来るって本当? FastAPI+GraphQL+テストで安全なAPI開発app.pyfrom fastapi import FastAPI import graphene from starlette.graphql import GraphQLApp from .graph import schema from .routers import user app = FastAPI() app.add_route('/graphql', GraphQLApp(schema=graphene.Schema(query=schema.Query, mutation=schema.Mutation))) app.include_router(user.router)schema.pyimport graphene from .serializers import UserInfoModel, UserModel, UserCreateModel class Query(graphene.ObjectType): get_user = graphene.Field(graphene.List(UserInfoModel), id=graphene.NonNull(graphene.Int)) @staticmethod def resolve_get_user(parent, info, id): return[ UserModel( id=1, user_name='t-yamada', first_name='taro', last_name='yamada' ) ] class CreateUser(graphene.Mutation): class Arguments: new_user = UserCreateModel() Output = UserInfoModel @staticmethod def mutate(parent, info, new_user): print(new_user) return UserModel( id=1, user_name='k-yamada', first_name='kenji', last_name='yamada' ) class Mutation(graphene.ObjectType): create_user = CreateUser.Field()serializers.pyfrom typing import List, Optional from graphene_pydantic import PydanticInputObjectType, PydanticObjectType from pydantic import BaseModel class UserModel(BaseModel): id: Optional[int] user_name: str first_name: str last_name: str class UserInfoModel(PydanticObjectType): class Meta: model = UserModel class UserCreateModel(PydanticInputObjectType): class Meta: model = UserModel exclude_fields = ('id', )試してみて
- 管理画面において複数グラフを表示する場合にRestだと複数エンドポイントにリクエストをしないといけないのがGraphQLだと一回で取得できそう
- Restだとレスポンスのフィールドが固定されている為、Reactなどで汎用的なテーブルコンポーネントを作成した場合にデータの間引きをする処理が必要になるが、GraphQLだと必要なフィールドのみを取得する事ができるのでフロントの実装が楽になりそう。
query { getUser(id: 1) { id userName firstName lastName } } // idを含めたくない場合のQuery query { getUser(id: 1) { userName firstName lastName } }
- 汎用的なテーブルコンポーネント
table.tsximport React from 'react' import Table, { Size } from '@material-ui/core/Table' import TableBody from '@material-ui/core/TableBody' import TableCell from '@material-ui/core/TableCell' import TableHead from '@material-ui/core/TableHead' import TableRow from '@material-ui/core/TableRow' import Button from '@material-ui/core/Button' import EditIcon from '@material-ui/icons/Edit' import styled from 'styled-components' import { Rows, Row, RowValue, Headers } from '~/types' type Props = { headers: Headers editable?: boolean size?: Size rows: Rows onClick?: (key?: number) => void } function mapRows(rows: Rows, headers: Headers): Rows { const columns = [] for (const header of headers) { columns.push(header.accessor) } const newRows = rows.map((row) => { const newHash: Row = { key: null, value: null } const newValue: RowValue = {} for (const key of columns) { newValue[key] = row.value[key] } newHash['value'] = newValue newHash['key'] = row.key return newHash }) return newRows } const DataGridView: React.FC<Props> = (props) => { const size = props.size || 'small' const isEditable = props.editable || false const newRows = mapRows(props.rows, props.headers) return ( <React.Fragment> <DataTable stickyHeader aria-label="sticky table" size={size}> <TableHead> <TableRow> {props.headers.map((value) => ( <TableCell key={value.accessor}>{value.header}</TableCell> ))} {isEditable && <TableCell />} </TableRow> </TableHead> <TableBody> {newRows.map((row) => ( <TableRow key={row.key}> {Object.keys(row.value).map((key) => ( <TableCell key={key}>{row.value[key]}</TableCell> ))} <TableCell> {isEditable && ( <Button size="small" color="primary" onClick={() => props.onClick(row.key)} > <EditIcon fontSize="small" /> </Button> )} </TableCell> </TableRow> ))} </TableBody> </DataTable> </React.Fragment> ) } const DataTable = styled(Table)` min-width: 750px; .MuiTableCell-stickyHeader { background: #f3f3f3; } ` export default DataGridView考察
まだまだQuery(取得)系以外のものに関しては調査等が必要だが、リアクトなどでフロントを作成する場合はSWRなどがGraphQLをサポートしてるのもあり、Rest APIと同時運用で特定ページのみGraphQLを採用し、徐々に移行しながら知見を深めていくのも良いかと感じました。
適用するサービスによりますが、社内システム等なら比較的採用しやすいのではと。今回は、比較的ライトに書かせて頂きましたが。今後また知見が深まった段階で記事を更新していければと思います。
- 投稿日:2021-02-25T05:00:30+09:00
【React】TypeScript を使用しているのに PropTypes での型定義は必要か?
結論
TypeScript での props の型定義も、PropTypes での型定義も、どちらも設定した方が吉です。
Typescript と PropTypes の違い
- TypeScript:コードを書いているときに役立ちます。
- TypeScriptの場合は、エディタでコードを書いているときに型チェックしてメッセージを出してくれますが、 buildした後は型チェックをしてくれません(例.DevTools の Console でのエラーメッセージの出力は無し)。
- PropTypes:たとえばAPIからJSONをロードする場合に役立ちます。
- PropTypesの場合は、buildした後にも DevTools の Console 等でエラーメッセージを出力してくれます。
検証してみた
※サンプルコードの内容は適度に省略してあります。
header.tsximport React from "react" import PropTypes from "prop-types" . . type Props = { siteTitle: string } & typeof defaultProps; const defaultProps = { siteTitle: `siteTitleが渡されなかった場合のデフォルトのタイトル`, } . , const Header: React.FC<Props> = ({ siteTitle }) => ( <h1>{siteTitle}</h1> ) . . // この propTypes を設定したりコメントアウトしたりして検証してみます。 Header.propTypes = { siteTitle: PropTypes.string.isRequired, }layout.tsximport React from "react" import PropTypes from "prop-types" import Header from "./header" . . // このsiteTitleは、本来ならstring型の値を渡さなければいけませんが、試しにnumber型の値を渡して意図的にエラーを吐いてくれるようにしてみます。 const Layout: React.FC = ({ children }) => ( <Header siteTitle={155} /> )PropTypes を定義した場合は、DevTools の Console で下記エラーメッセージを出力して怒ってくれますが、
index.js:2177 Warning: Failed prop type: Invalid prop
siteTitle
of typenumber
supplied toHeader
, expectedstring
.
in Header (at layout.tsx:28)
in Layout (at pages/index.tsx:9)
.
.PropTypesを設定していない場合は、何も怒ってくれません。
よって、TypeScriptの使用の有無に関わらず、PropTypesも設定しておいた方が無難そうです。
(二重チェックになるので不必要である、と言うことは無さそうです。)参考にさせていただいたURL
https://github.com/Microsoft/TypeScript/issues/4833#issuecomment-282497313
- 投稿日:2021-02-25T02:18:16+09:00
React: メモ化の前に考えるふたつの基本的な手法
Dan Abramov氏による記事「Before You memo()」が2月23日付で公開されました。描画を最適化するために、
memo
やuseMemo
を使うことは有効です。けれど、その前に考えられる基本的な手法をふたつ紹介されています。記事に示されたコード例を、かいつまんでご説明してみます。重たいコンポーネントが考えなしに放り込まれたアプリケーション
はじめにとりあげられるのが、重たいコンポーネントを考えなしに放り込んだつぎのコード例です。
ExpensiveTree
が負荷の高いコンポーネントを意味します。import { useState } from 'react'; export default function App() { let [color, setColor] = useState('red'); return ( <div> <input value={color} onChange={(e) => setColor(e.target.value)} /> <p style={{ color }}>Hello, world!</p> <ExpensiveTree /> </div> ); }コード例のコンポーネント
ExpensiveTree
は、performance.now()
で意図的に負荷を高めています。前掲のコンポーネントApp
は、状態が変わるたびに、この重たいコンポーネントも再描画しなければならないわけです。function ExpensiveTree() { let now = performance.now(); while (performance.now() - now < 100) { // Artificial delay -- do nothing for 100ms } return <p>I am a very slow component tree.</p>; }Dan Abramov氏は、この作例からはじまるステップごとに、コードをCodeSandboxに公開しています。
状態を下層に移す
そこで、この問題に対処するひとつめの手法です。状態とその操作を子コンポーネント(
Form
)に切り分けて、下層に移します。すると、親コンポーネントからは状態がなくなり、子コンポーネントだけが再描画すれば済むのです。export default function App() { // let [color, setColor] = useState('red'); return ( <div> {/* <input value={color} onChange={(e) => setColor(e.target.value)} /> <p style={{ color }}>Hello, world!</p> */} <Form /> </div> ); } function Form() { let [color, setColor] = useState('red'); return ( <> <input value={color} onChange={(e) => setColor(e.target.value)} /> <p style={{ color }}>Hello, world!</p> </> ); }こちらがCodeSandboxの作例です。
親に状態をもたせたいとき
問題は、親に状態をもたせたいときです。このような例として、つぎのコードが示されています(作例)。
export default function App() { let [color, setColor] = useState('red'); return ( // <div> <div style={{ color }}> <input value={color} onChange={(e) => setColor(e.target.value)} /> {/* <p style={{ color }}>Hello, world!</p> */} <p>Hello, world!</p> <ExpensiveTree /> </div> ); }この場合に使えるふたつめの手法です。やはり、状態は親から子コンポーネント(
ColorPicker
)に切り出します。そして、負荷の高いコンポーネントや要素もその中に包んでしまうのです。export default function App() { // let [color, setColor] = useState('red'); return ( // <div style={{ color }}> <ColorPicker> {/* <input value={color} onChange={(e) => setColor(e.target.value)} /> */} <p>Hello, world!</p> <ExpensiveTree /> {/* </div> */} </ColorPicker> ); }子コンポーネント(
ColorPicker
)の状態とその操作は、前の例と変わりません。注目いただきたいのは、コンポーネントが親からchildren
プロパティを受け取ることです。そして、このプロパティは、状態をもたない親が変更することはありません。その場合、子コンポーネントの中でもchildren
の再描画は行われないのです。function ColorPicker({ children }) { let [color, setColor] = useState('red'); return ( <div style={{ color }}> <input value={color} onChange={(e) => setColor(e.target.value)} /> {children} </div> ); }これで、重たいコンポーネントの再描画は避けることができました。こちらが、CodeSandboxの作例です。変化するものとしないものを分けるという基本的な考え方が、このふたつの手法に通じているといえるでしょう。
- 投稿日:2021-02-25T01:12:40+09:00
【Spring Boot】 海外Teck系YouTuberを真似てみた!ReactJS+SpringBoot Full Stack Application
1,はじめに
この記事は、海外Teck系YouTuberの動画を参考に、同じプロジェクトを作成してみたものになります!
簡単にですが、動画を通して学べた技術や知識をまとめました!2,学んだこと
主に、
① Axios HTTP Libraryを導入して、ReactとSpringBootを連携させる。
② Postmanを使ってRESTApiの動作を確認する。その他にも、npmを使ってrouterやaxiosのインストール方法も学べました!
ゴールが見えているので、モチベーションにもなって効率良く学習が進めれました!
真似るだけでも、様々な技術の習得に繋がったのでぜひ、ぜひトライしてみてください!完成したものは、下記に載せています
3,参考動画
URL: https://youtube.com/playlist?list=PLGRDMO4rOGcNLnW1L2vgsExTBg-VPoZHr
4,Tools and Technologies
【フロントエンド】
React
Modern JavaScript(ES6)
NodeJS and NPM
VS Code IDE
Create React App CLI
BootStrap 4.5 and Axios HTTP Library【バックエンド】
Spring Boot 2+
SpringData JPA(Hibernate)
Maven 3.2+
JDK 1.8
Tomcat 8.5+
MySQL Database5,完成
補足
Postmanを使うと、
ネットワーク経由で外部のサーバーにアクセスし、必要な情報を取得することができます!
URL: Postman