コンテキストフックの利用
前回まででVtuberを一覧画面を表示させるところまで実装しましたが、今回は個別の詳細画面まで遷移させるところまで実装します。今回は一覧画面と詳細画面でページ間のデータ共有を行う必要があったので、チュートリアル外ですがコンテキストフックを使用して実現します。
以下、左からトップ画面、一覧画面、詳細画面です。尚、Serverless Frameworkを使ったAPI(Lambda+API Gate Way + DynamoDB)の作成については別トピックとして記事にしようと思います。
まずはコンテキストによるデータの共有を行うための準備をします。データ共有の目的ですが、一覧画面でAPIから取得した情報を個別画面で参照し、対象の情報だけ表示させるためです。
ただし、Nextjsはファイルベースのルーティング(ディレクトリ、ファイルの構成がそのままパスになる)を行っているため、ページ間でデータ共有を行いたい場合はどうしても後述する_app.tsx
に設定を追加する必要があり、共有するデータが多いほど_app.tsx
が汚くなります。
また、グローバルなのですべてのページで情報が参照できるのでそういったケースは以下のようにそもそも設計を見直したりした方が良いのかもしれません。
- 引き継ぎたい情報が少ない場合はパスパラメータやクエリパラメータを使って遷移先に渡す
- 遷移先で別途APIを使って情報を取得する
今回のケースの場合、各情報を識別するIDのみクエリパラメータ(orパスパラメータ)で一覧画面から詳細画面に渡す。遷移後、IDを使用してDBから情報を1件取得するAPIを叩いてデータを持ってくる、といった具合でしょうか。
今回は一覧の情報は更新頻度が高くないと仮定しているので再度APIにリクエストを投げずにデータ共有を行うようにしています。
コンテキストフックを使用するためにsrc/contextフォルダを作成し、以下の3つのファイルを作成します。(構成は以下)
src
├── app.const.ts
├── components
├── context
│ └── vtuber-context
│ ├── vtuber-context-provider.tsx
│ ├── vtuber-context-type.ts
│ └── vtuber-context.ts
# 省略
まずはvtuber-context-type.tsです。このファイルではコンテキストで共有する情報の型定義を行っています。今回はAPIで取得したArrayと情報更新用のメソッドを定義しています。
import { VtuberInfo } from '../../data/vtuber-info-type';
/** VtuberInfo情報をページ間で共有するコンテキストType */
export type VtuberCopntextType = {
vList: VtuberInfo[];
updateVList: (vtuberInfoList: VtuberInfo[]) => void;
};
続いてvtuber-context.tsですが、コンテキストの実態を生成するメソッドを定義しています。Arrayの情報はuseStateフックを使用し、更新メソッドの実装もここで行っています。
import { useState } from 'react';
import { VtuberInfo } from '../../data/vtuber-info-type';
import { VtuberCopntextType } from './vtuber-context-type';
/** VtuberInfoのコンテキスト定義 */
export const useVCtx = (): VtuberCopntextType => {
const [vList, setVlist] = useState<VtuberInfo[]>([]);
const updateVList = (list: VtuberInfo[]) => {
setVlist(list);
};
return { vList, updateVList };
};
最後にvtuber-context-provider.tsxですが、ここではプロバイダの定義を行っています。このプロバイダでアプリケーション全体をラップすることで各ページでコンテキストを参照することができるようになりうます。(14行目でコンテキストを生成してvalueに設定しています。)
また、コンテキストの型情報チェックを自動で行いたかったため、下記の記事を参照してuseContextフックを使用してコンテキストとプロバイダを生成するUtilityメソッド(7行目)を作成しています。
【React】デフォルト値もundefinedチェックもいらないcreateContext【Typescript】
Reactフックの各フックの使い方はフック API リファレンスがわかりやすいです。
import { NextPage } from 'next';
import { createCtx } from '../../utils/create-context';
import { useVCtx } from './vtuber-context';
import { VtuberCopntextType } from './vtuber-context-type';
export const [useVtuberContext, SetVtuberProvider] =
createCtx<VtuberCopntextType>();
type Props = {
children?: React.ReactNode;
};
export const VtuberContextProvider: NextPage = ({ children }: Props) => {
const vtuberContext = useVCtx();
return (
<SetVtuberProvider value={vtuberContext}>{children}</SetVtuberProvider>
);
};
後はアプリ全体をラップしてページからコンテキストを参照するだけです。
アプリ全体のラップは次のようにします。12行目で先ほど生成したVtuberContextProviderにて下位コンポーネントを包んでいます。先ほどデータ共有が多くなると見直した方が良いと言っていたのは共有したい情報が増えるほどここで各コンテキストのProviderがネストしてしまうためです。
(コンテキストを一つにまとめてしまう、といったこともできますがそれはそれで肥大化して何でも入りコンテキストが生まれてしまうのでいまいちな気がしています。)
あまり実践でがりがりNextjs書いているわけではないですが、今回触ってみてコンテキストフックについて以下のような感覚を持ちました。
- できるだけページ間での共有は最小限に抑える(URLに埋め込むレベル)
- APIを活用してページごとに情報を取得(リクエストの負荷と相談)
- セッションで保持するようなグローバルに共有したい情報(ログイン状態・ユーザ情報)はコンテキストで持つ
import { NextPage } from 'next';
import type { AppProps } from 'next/app';
import '../styles/globals.css';
/** ページレイアウト定義 */
import PageLayout from '../src/components/page-layout/pageLayout';
/** プロバイダ */
import { VtuberContextProvider } from '../src/context/vtuber-context/vtuber-context-provider';
const MyApp: NextPage<AppProps> = ({ Component, pageProps }: AppProps) => {
return (
<VtuberContextProvider>
<PageLayout>
<Component {...pageProps} />
</PageLayout>
</VtuberContextProvider>
);
};
export default MyApp;
ページでのコンテキストの取得は以下となります。以下は一覧画面の内容ですが、APIで取得した情報でコンテキストの情報を更新しています。
import type { InferGetStaticPropsType, NextPage } from 'next';
import Link from 'next/link';
import { getVtuberInfoList } from '../../src/lib/api/get-vtuber-info-list';
import ListElement from '../../src/components/vtuber-Info/list-element';
import { useVtuberContext } from '../../src/context/vtuber-context/vtuber-context-provider';
type Props = InferGetStaticPropsType<typeof getStaticProps>;
export const getStaticProps = async () => {
// 省略
};
const VtuberInfoListPage: NextPage<Props> = ({ vtuberInfoList }) => {
/** コンテキスト情報の更新 */
const { vList, updateVList } = useVtuberContext();
updateVList(vtuberInfoList);
return (
<>
<h1 className="title">Vtuber List</h1>
動的ルーティング
- +αのコンテキストが長くなってしまいましたが、本題のチュートリアルに戻ります。
動的ルーティングではリクエストのURLに応じてページを生成します。例えば/vtuber-info/[pageId]というパスの場合、pages/vtuber-info/[pageId].tsxといったファイルを作成することで動的にページを返します。いかが作成例になります。
コンテキストから情報を取得し、URLのpageIdを取得、配列の番号に変換(Utilityを作成して実施)することで画面に情報を表示しています。また、Pathからの情報取得時に型チェックを実施したかったので以下のページがとても分かりやすかったため参考にしてパスを扱うUtilityを作成しています。
import Link from 'next/link';
import Image from 'next/image';
import { NextPage } from 'next';
import { URL_YOUTUBE_CHANNEL } from '../../src/app.const';
import { useVtuberContext } from '../../src/context/vtuber-context/vtuber-context-provider';
import { useRouter } from 'next/router';
import { convertPageIdToListNum } from '../../src/utils/vtuber-utils';
import { usePathParams } from '../../src/utils/get-path';
const vtuberInfoDetailsPage: NextPage = () => {
/** コンテキスト情報の取得 */
const { vList, updateVList } = useVtuberContext();
const router = useRouter();
// パスパラメータから値を取得
const { pageId } = usePathParams<'vtuber', { name: string }>();
const listNum = convertPageIdToListNum(pageId);
return (
<>
<h1 className="title">Vtuber Details</h1>
<h2>
{vList[listNum].vtuberId} : {vList[listNum].youtubeInfo.title}
<Image
priority
src={vList[listNum].youtubeInfo.icon}
height={50}
width={50}
alt={vList[listNum].youtubeInfo.title}
/>
</h2>
<ul>
<li>
<Link
href={URL_YOUTUBE_CHANNEL + vList[listNum].youtubeInfo.channelId}
>
YouTube
</Link>
</li>
<li>
<Link href={vList[listNum].twitterInfo.url}>Twitter</Link>
</li>
</ul>
</>
);
};
export default vtuberInfoDetailsPage;
API
今回特に使用したいケースがイメージできなかったたのですがNextjsではAPIサーバの役割も果たしてくれるようです。pages/api/handler.tsを作成し、以下のように実装します。アプリを起動して/api/handlerにアクセスするとレスポンスが返ってきます。
import type { NextApiRequest, NextApiResponse } from 'next';
const api = (req: NextApiRequest, res: NextApiResponse) => {
res.status(200).json({ name: 'API works!' });
};
export default api;
チュートリアル外の部分が長くなってしまいましたが今回は以上です。あとはデプロイだけかな?