

teratailとのクロスポストです。  
<https://teratail.com/questions/m36tsrglghhmol>

### 実現したいこと

  * ページ単位で状態を共有する（実装済みだが、Next.jsの枝葉をClient Componentにして幹はServer Componentにするという原則に反してしまっているので、できれば解消したい）。
  * ブラウザでページロード時に即座に最新の情報が表示されるようにするため、サーバー側で最新の情報を用いてHTMLをレンダリングする（必ずしも必要な機能ではないが、技術的チャレンジとして）。
  * APIをポーリングし、表示を更新する（実装済みだが、 `useEffect` を使うことでClient Componentになってしまう）。

### 前提

Next.js 13のApp Routerを用いてWebアプリを開発していますが、上記の要件を満たす方法が分かりません。

### 該当のソースコード

[code]

    import { QueueItem } from './types';
    
    export default class Manager {
      static current = new LiveStreamManager();
    
      #items: QueueItem[] = [];
    
      private constructor() {
        this.fetchItems();
        setInterval(() => this.fetchItems(), 600 * 1000); // 10mins
      }
    
      getItems(): QueueItem[] {
        return this.#items;
      }
    
      async fetchItems() {
        ...
      }
    
[/code]

フロントエンドで再生するアイテムを管理するサーバー側のクラスです。

[code]

    import { NextResponse } from "next/server";
    
    import Manager from "@/app/api/Manager";
    
    export async function GET() {
      let manager = Manager.current;
      return NextResponse.json(manager.getItems());
    }
    
[/code]

APIです。

[code]

    'use client';
    
    import { useState, useEffect } from 'react';
    
    import { QueueItem } from '@/app/api/types';
    
    import Player from './components/Player';
    import QueueList from './components/QueueList';
    
    function App() {
      console.log('rendering App');
    
      const [items, setItems] = useState<QueueItem[]>([]);
      const [currentItemID, setCurrentItemID] = useState('');
    
      useEffect(() => {
        refresh();
        let timer = setInterval(refresh, 10000);
        return () => { clearInterval(timer); };
      // eslint-disable-next-line react-hooks/exhaustive-deps
      }, []);
    
      async function refresh() {
        console.log('refresh');
    
        let items = await fetch(`/api/items`).then(res => res.json()) as QueueItem[];
        setItems(items);
        if (currentItemID === '' && items.length > 0) {
          setCurrentItemID(items[0].id);
        }
      }
    
      return (
        <div className="h-screen flex bg-slate-900 text-white text-sm">
          <Player
            className='grow h-full'
            videoID={currentItemID}
            onEnded={() => {
              ...
            }}
          />
          <QueueList
            items={items}
            currentQueueID={currentItemID}
            setQueueID={setCurrentItemID}
          />
        </div>
      );
    }
    
    export default App;
    
[/code]

フロントエンドです。Client Componentに指定しています。

### 試したこと

以下のように書き換えてみました。

[code]

    async function App() {
      console.log('rendering App');
    
      // const [items, setItems] = useState<QueueItem[]>([]);
      let items = await fetch(`/api/streams`, { next: { revalidate: 10 } }).then(res => res.json()) as QueueItem[];
      const [currentItemID, setCurrentItemID] = useState('');
      const videoID = items.find(item => item.id === currentItemID)?.media.videoID;
    
      return (
        ...
      );
    }
    
[/code]

サーバーにおいてレンダリングされているようですが、 `TypeError: Failed to parse URL from /api/streams`
というエラーが出ます。また、ブラウザ側で再描画とネットワークリクエストが無限ループします。

### 補足情報（FW/ツールのバージョンなど）

[code]

      "dependencies": {
        "next": "13.4.6",
        "react": "18.2.0",
        "react-dom": "18.2.0"
      }
    
[/code]


CC BY-SA 4.0
2023-06-22T03:06:21.057
95322
2023-07-04T21:22:03.263
2023-06-22T05:26:30.987
8099
8099
question
next.js
Next.js 13のApp Routerでサーバー側で最新の情報を用いてHTMLをレンダリングし、かつポーリングして表示更新する


# 1\. ページ単位で状態を共有する

`app/page.tsx` を単一のComponentを返すServer Componentとする。

[code]

    async function Page() {
      return <App />;
    }
    
    export default Page;
    
[/code]

# 2\. ブラウザでページロード時に即座に最新の情報が表示されるようにするため、サーバー側で最新の情報を用いてHTMLをレンダリングする

`app/page.tsx` はServer Componentなので、サーバー側の関数が使える。

[code]

    async function Page() {
      console.log('rendering Page');
    
      const streams = await getLiveStreamItems();
      const videos = await getVideoItems();
    
      return (
        <App
          initialStreams={streams}
        />
      );
    }
    
    export default Page;
    
[/code]

# 3\. APIをポーリングし、表示を更新する

[SWR](https://swr.vercel.app/ja)というライブラリを使う。

[code]

    'use client';
    
    import { useState } from 'react';
    import useSWR from 'swr';
    import type { SWRConfiguration } from 'swr';
    
    import fetcher from '@/app/utils/fetcher';
    
    import Player from './Player';
    import List from './List';
    
    type Props = {
      initialStreams: QueueItem[];
    };
    
    function App(props: Props) {
      const { initialStreams } = props;
      const [currentItemID, setCurrentItemID] = useState('');
    
      // https://swr.vercel.app/ja/docs/typescript#generics
      let config: SWRConfiguration = {
        refreshInterval: 60_000, // 1 minute
        fallbackData: initialStreams,
      };
      let { data: streams } = useSWR<QueueItem>('/api/streams', fetcher, config);
      streams = streams ?? [];
    
      return (
        <div>
          <div className="flex">
            <Player
              className='grow'
              videoID={videoID}
              onEnded={() => {...}}
            />
          </div>
          <List
            className='m-4 mx-auto'
            streamItems={streams}
            currentItemID={currentItemID}
            setItemID={setCurrentItemID}
          />
        </div>
      );
    }
    
    export default App;
    
[/code]


CC BY-SA 4.0
2023-07-04T21:22:03.263
95469
2023-07-04T21:22:03.263
8099
95322
answer