LoginSignup
3
2

More than 1 year has passed since last update.

【Next.js】ページごとに異なるAPIを呼び出して見せ方を変える実装メモ

Last updated at Posted at 2022-03-18

自分のところの/api/配下のAPIを実行してデータのやり取りをするアプリケーションにおいて、いろんなAPIを汎用的に呼び出せるようにしたうえで、フロント側の見せ方はページごとに変えたいと思っていた。
調べた感じズバッとこれだ!っていうのがなかったのだが、色々やった感じで以下の方法でうまく動作したので、メモとして残す。

単一APIの利用

テスト用のAPIを用意

src/pages/api/hello.ts
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'

type Data = {
  name: string
}

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<Data>
) {
  res.status(200).json({ name: 'John Doe' })
}

  • npx create-next-app --typescriptで作成したときにできるやつをそのまま使用。
  • 中身は実際なんでもいい。ここでは動作確認用のためこれを使用する。

APIを実行して結果を加工して返却するためのcomponent

src/components/api-component.tsx
import useSWR from 'swr';

const fetcher = async (uri: string) => {
    const response = await fetch(uri);
    return response.json();
};

export function ApiComponent(uri: string, RF: React.FC<any>) {
    const { data, error } = useSWR(uri, fetcher);
    if (error) return <div>oops... {error.message}</div>;
    if (data === undefined) return <div>Loading... with React.FC</div>;
    return <RF data = { data } />;
}
  • swrに関してはこちらの実装をほぼそのまま流用。全てのAPIの実行をこれに寄せることで、エラー発生時やロード中の表示を統一できる。
  • APIの実行結果を加工と書いているが、加工処理の実態は引数で渡されたRF: React.FC<any>に委ねられている。そしてこれはこのApiComponentを呼び出す側から渡される形。
  • React.FCの型を<any>にしているのは、いろんなAPIの結果(いろんなオブジェクトの形)を受け取ることを意識して。統一できそうにないなと思ったので。統一の型があるならその型を指定する。

APIの実行指示と実行結果の画面編集を行うpage

src/pages/test.tsx
import type { NextPage } from 'next'
import { ApiComponent } from '../components/api-component';

const Test: NextPage = () => {
    return ApiComponent('/api/hello' , TestContent);
}

type Props = {
    data: {
        name: string
    }
};

const TestContent: React.FC<Props> = (props) => {
    const { data } = props;
    return (
        <>
            <p>name: {data.name}</p>
        </>
    );
}

export default Test;
  • ApiComponentには、引数としてAPIの実行パス('/api/hello')と画面表示内容のReactComponent定義(TestContent)を渡す。
  • APIの実行パスは一番上で書いたやつを呼びだす。実際のプロジェクトの実態に応じて変更要
  • TestContentは実際に画面にどう表示するかの編集ロジック。ApiComponentの引数に渡して、編集してもらって結果を返してもらう。処理の内容自体はここで定義。つまり、ページごとに定義する。この例では<p>{JSON.stringify(data)}</p>と、非常に簡単な処理しか実装してないが、各ページの仕様に従って色々加工する。
  • Propsのtype定義は呼び出すAPIの返却オブジェクトの仕様をあらわしている。ここはAPIごとに変わるので適宜変更。ここに書くよりはAPI側のほうにtype定義してそれをExportさせて使う方がいいかもしれない。
  • これだけだと単にname: John Doeとしか表示しないので、実際にはlayout componentとかと組み合わせて画面内の適切な箇所に適宜表示するようにTestContent内の定義を変更する必要がある

追記:複数APIの利用

上のは特定のページで単一のAPIのみ利用するパターンだが、単一のページで複数のAPIを使いたい場合どうするか?というのをメモする。基本的には変わらない。

テスト用のAPIを用意

ちょっと違うAPIを用意するがリクエストパラメータを貰う以外はほぼ↑と同じ。

src/pages/api/test.ts
import type { NextApiRequest, NextApiResponse } from 'next'

export type TestAPIType = {
  text: string
}

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<TestAPIType>
) {
  const query = req.query;
  const text = String(query.text||'(no-data)');

  res.status(200).json({ text: text });
}
  • リクエストパラメータとして text を受け取ってそれをそのままレスポンスするだけのAPI。リクエストパラメータを使ったのは、これから「複数のAPI」を利用する想定にあたり、それぞれのAPI呼び出しがちゃんと動作しているかを確認するためのもので、機能上の意味は全くない。なんならその数の分だけAPI用意しても良い。(本来の開発においてはそっちのほうが用途が多そうではある)
  • ジェネリクスで使用している TestAPITypetext: string で定義しているため、 const text = String(query.text||'(no-data)'); とわざわざ String で囲っている( NextApiRequest#querystring | string[] なので、そのまま使うとType定義に反してしまう)と思ったけどだったら TestAPITypetext: string|string[] にすりゃいいだけか。。まあいいや。

APIを実行して結果を加工して返却するためのcomponent

src/pages/components/test.tsx
import { ApiComponent } from './api-component';
import { TestAPIType } from '../pages/api/test';

export const Test1 = () => {
    return ApiComponent('/api/test?text=test1' , TestContent1);
};

export const Test2 = () => {
    return ApiComponent('/api/test?text=test2' , TestContent2);
};

export const Test3 = () => {
    return ApiComponent('/api/test?text=test3' , TestContent3);
};

type Props = {
    data: TestAPIType
}

const TestContent1: React.FC<Props> = (props) => {
    const { data } = props;
    return (
        <>
            <div>
                <p>test1</p>
                <p>text: {data.text}</p>
            </div>
        </>
    );
}

const TestContent2: React.FC<Props> = (props) => {
    const { data } = props;
    return (
        <>
            <div>
                <p>test2</p>
                <p>text: {data.text}</p>
            </div>
        </>
    );
}

const TestContent3: React.FC<Props> = (props) => {
    const { data } = props;
    return (
        <>
            <div>
                <p>test3</p>
                <p>text: {data.text}</p>
            </div>
        </>
    );
}
  • さっきの例ではpage側で使っていた ApiComponent をここで使う。基本的にはpageのほうでやっていた実装例と同じ。つまり、pageに返すより前に、component側でpageに返す情報を先んじて作っておく。pageは単にそれを利用するだけ。
  • この例では「複数のAPI」として3つの異なるAPIを呼び出す想定。export const Test1 = () => { ... の流れの3つがそれに相当する。やってることは /api/test?text=test1 と、先ほどのAPIにリクエストパラメータ付きで呼び出して、結果を加工して編集してもらって(ただし編集定義はここに定義している const TestContent1: React.FC<Props> = (props) => { ... を渡す)返す。そんだけ。
  • const TestContent1: React.FC<Props> = (props) => { ... の部分はほとんど変わり映えしてなくて、中身の <p>test1</p> とかの部分だけが違っているだけ。これは「複数のAPIが(一応)それぞれに応じた編集定義をもってページ情報を生成している」ことを確認したかっただけの意図しかない(ここの、文字列部分で違いを見分けるため)実際にはもっと凝ることになると思う。。

APIの実行指示と実行結果の画面編集を行うpage

src/pages/testx.tsx
import { Test1 , Test2, Test3 } from '../components/test';
import type { NextPage } from 'next';

const TestX:NextPage = () => {
    return (
        <>
                {Test1()}
                {Test2()}
                {Test3()}
        </>
    );
};

export default TestX;
  • これは滅茶苦茶簡素である。既にやるべきことは上の2つで終わっているからだ。このページはそれを呼び出す役割しか持っていない。呼び出し方はimportして {Test1()}とかするだけ。これだけで1ページからTest1~Test3の3つのAPIを呼び出す画面の出来上がりだ。

環境

  • RemoteContainerで開発。ベースイメージはnode:16-bullseye-slim
  • "next": "12.0.10",
  • "react": "17.0.2",
  • "react-dom": "17.0.2",
  • "swr": "^1.2.2"
3
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2