Next.js14 Server ComponentでのData Fetching

Next13からstableとなったAppRouterを使い、ServerComponentでバックエンドのRailsにAPIリクエストを送る処理を実装したいと思います。

今回、チーム開発でフロントエンドにNext.js14を導入することになりました。

なるべく公式に沿って実装しようということで、AppRouterのServerComponentをフル活用する運びとなりましたが、以前作った自分のPFも、PageRouterを参考にしたためかapp/page.tsxで'use client'を使用しています。ナンテコッタ。これではせっかくのAppRouterが…。

ということで、ServerComponentの使い方を調べつつ、バックエンドのRailsにAPIリクエストを送る実装を作ってみることにしました。


## 環境
Next.js 14.1.4
Rails 7.1.3
Docker

## Server Componentをサラッとおさらい
このあたりは詳しく書かれた記事もたくさんあるので割愛
公式によると、
> AppRouterのデフォルトではサーバーコンポーネントを使用します。これにより、追加の構成を行わずにサーバーレンダリングを自動的に実装できるようになり、必要に応じてクライアントコンポーネントの使用を選択できます。公式

サーバーコンポーネントのメリットはこちら

https://nextjs.org/docs/app/building-your-application/rendering/server-components

ということで具体的には、
・インタラクティブ機能とイベントリスナー(onClick、onChangeなど)を使う
・状態とライフサイクルの影響 (useState、useReducer、useEffectなど)を使用する
・ブラウザ専用 API を使用する
・状態、効果、またはブラウザ専用 API に依存するカスタム フックを使用する
・Reactクラスコンポーネントを使用する

これら以外ではServer Componentを使用するようにします。

https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns

## バックエンド実装
まず、バックエンドのRailsでリクエストを受け取って処理する機能を簡単に実装します。

```
back $ docker-compose run --rm back bundle exec rails g scaffold test_post title:string
```
backディレクトリに移動し、scaffoldでファイルを作成し、`rails db:migrate`します。

次にseedファイルを作成します。
```ruby
Test_post.create!(
  [
    { title: 'Ruby' },
    { title: 'Rails' },
    { title: 'Next.js' },
    { title: 'React' }
  ]
)
```
こちらを`rails db:seed`します。

今回、getメソッドでindexにアクセスし、title一覧を獲得します。
controllerはindexのみ使用します。

```ruby
class TestPostsController < ApplicationController

  def index
    @test_posts = TestPost.all

    render json: @test_posts
  end
end
```

## フロントエンド実装
公式ドキュメントのData Fetchingのサンプルコードを見ると、
```react:app/page.tsx
async function getData() {
  const res = await fetch('https://api.example.com/...')
  // The return value is *not* serialized
  // You can return Date, Map, Set, etc.
 
  if (!res.ok) {
    // This will activate the closest `error.js` Error Boundary
    throw new Error('Failed to fetch data')
  }
 
  return res.json()
}
 
export default async function Page() {
  const data = await getData()
 
  return <main></main>
}
```
app/page.tsxですべて完結されています…。しかし、作成するアプリではロジックとビューのコンポーネントを分割し、app/page.tsxで呼び出して使用したい。

### 
ということで、fetchの部分をコンポーネントに切り出して作成することにしました。

```react
interface Post {
  title: string;
}

async function getTestPost() {
  const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/test_posts`);
  const posts: Post[] = await response.json();
  return posts.map(post => post.title);
}

export default async function TestPostUI() {
  const titles = await getTestPost()
  
  return (
    <div>
      <h1>Post Titles</h1>
      <ul>
          {titles.map((title, index) => (
              <li key={index}>{title}</li>
          ))}
      </ul>
    </div>
  );
}
```

### リクエスト先のURLの指定
ここで最初の問題は、リクエストを送るURLの部分です。
クライアント側からリクエストする際は、`localhost:◯000/test_posts`を指定すればokだったのですが、今回サーバー側からサーバー側へのリクエスト(?)になるためかアクセスできませんでした。

**【参考】**
> frontのNext.jsアプリケーションからbackのAPIにアクセスしたい場合、frontコンテナ内のコードでhttp://localhost:4000と指定しても、それはfrontコンテナ内を指してしまうため、backサービスには届きません。これはlocalhostが各コンテナにとって自分自身を指すため、別のコンテナ（この場合はbackコンテナ）にはアクセスできないからです。GPT

https://qiita.com/parkon_hhs/items/b40c6a87c05d5a34867c

実際にlocalhostではなく、バックエンドのサービス名を使い、back:3000と指定するとうまくアクセスできました。また、docker-compose.ymlでポート番号をマウントしている場合、例えば
```
  ports:
      - "4000:3000"
```
この場合、Dockerの実際のポート番号は3000になるため、URLのサービス名にback:3000を指定します。

### ビュー部分をapp/page.tsxで呼び出す
```react
import TestPostUI from './_services/testPost';

export default function Home() {
  return (
    <>
      <TestPostUI />
    </>
  );
}

```
[![Image from Gyazo](https://i.gyazo.com/9b9f9218772a7415bd847b9913749d6b.png)](https://gyazo.com/9b9f9218772a7415bd847b9913749d6b)

これでうまく値を獲得し表示させることができました。
ちゃんとサーバーサイドでレンダリングできているか確認するために、`TestPostUI`のtitlesをコンソールに出力してみます。
```react
const titles = await getTestPost()
console.log(titles)
```
[![Image from Gyazo](https://i.gyazo.com/0d5e98d2f7f5a139e7390f14c6615065.png)](https://gyazo.com/0d5e98d2f7f5a139e7390f14c6615065)

サーバー側のlogに値が出力され、ブラウザのconsoleには何も表示されませんでした。
ということで、この方法でサーバー側でfetchし、HTMLの生成もサーバー側で行うことに成功しました。パチパチ！

## うまくいかなかったビューの切り分け
でもまてまて、本当は、ロジック部分とビュー部分も別コンポーネントで管理したいところです。
ということで、APIコンポーネントからビューの部分を切り出し、fetchしたデータをpuropsでUIに渡すようにしてみました。
```react
interface Post {
  title: string;
}

export default async function getTestPost() {
  const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/test_posts`);
  const posts: Post[] = await response.json();
  return posts.map(post => post.title);
}
```
```react
interface TestPostUIProps {
  titles: string[];
}

export default function TestPostUI({ titles }: TestPostUIProps) {
  return (
    <div>
      <h1>Post Titles</h1>
      <ul>
        {titles.map((title, index) => (
          <li key={index}>{title}</li>
        ))}
      </ul>
    </div>
  );
}

```

こちらは結果うまく動かすことができませんでした。
```
TypeError: Cannot read properties of undefined (reading 'map')at TestPostUI
```

これはtitlesの値がpropsで渡ってきておらず、map関数を呼び出す際にtitlesが`undefined`であるとエラーになっています。logを確認すると、そもそもSQLも発行されていません。

ここで詰まって色々ゴニョゴニョしたところ、fetch関数が発火してない？ような感じで、'useEffect'を挟むとちゃんと取れてくる…ﾃﾞﾓクライアントレンダリングになっちゃう…

ということで、まあ公式もロジックとビューを同じファイルで書いてるし、一旦諦めて(笑)先程のコードで提出することになりました。

## TypeScriptのエラー
さて、ちゃんとデータを獲得できてほっとしたのもつかの間、app/page.tsxが真っ赤になっています。
[![Image from Gyazo](https://i.gyazo.com/07b0932c249b1d738305aaf56954be17.png)](https://gyazo.com/07b0932c249b1d738305aaf56954be17)

もう勘弁してくり。

`<TestPostUI />`の呼び出し部分に以下の型エラーが発生しています。
```
'TestPostUI' を JSX コンポーネントとして使用することはできません。
  その戻り値の型 'Promise<Element>' は、有効な JSX 要素ではありません。
```
調べてみると、Next13の時点では確認されているエラーのようで、TypeScriptの型との互換性に起因しているようでした。

https://zenn.dev/waarrk/articles/23732d8c4102d0

しかし、現在はNext14で、この記事も1年以上前のものです。まだ残ってるのか？そんな馬鹿な？


https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating

> To use async/await in a Server Component with TypeScript, you'll need to use TypeScript 5.1.3 or higher and @types/react 18.2.8 or higher.

現在の公式にはこのようにあるので、原因はここかも？

この問題はまだ未解決ですが一旦参考記事にあるようにエラーを逃がす方法を採用しました。
```react
import TestPostUI from './_services/testPost';

export default function Home() {
  return (
    <>
      {/* @ts-expect-error Server Component */}
      <TestPostUI />
    </>
  );
}
```
もっとスッキリ解決できたら更新したいと思います。


## 最後に
最後までお読みいただきありがとうございます。
忘れないうちに勢いでまとめたので至らない点が多いかと思います。
ご指摘おまちしております。