回答を迅速に得るため、StackOverflowとのクロスポストを行っております。
https://ja.stackoverflow.com/questions/95322
実現したいこと
- ページ単位で状態を共有する(実装済みだが、Next.jsの枝葉をClient Componentにして幹はServer Componentにするという原則に反してしまっているので、できれば解消したい)。
- ブラウザでページロード時に即座に最新の情報が表示されるようにするため、サーバー側で最新の情報を用いてHTMLをレンダリングする(必ずしも必要な機能ではないが、技術的チャレンジとして)。
- APIをポーリングし、表示を更新する(実装済みだが、
useEffect
を使うことでClient Componentになってしまう)。
前提
Next.js 13のApp RouterでWebアプリを開発していますが、上記の要件を満たす方法が分かりません。
該当のソースコード
typescript:app/api/Manager.ts
1import { QueueItem } from './types'; 2 3export default class Manager { 4 static current = new LiveStreamManager(); 5 6 #items: QueueItem[] = []; 7 8 private constructor() { 9 this.fetchItems(); 10 setInterval(() => this.fetchItems(), 600 * 1000); // 10mins 11 } 12 13 getItems(): QueueItem[] { 14 return this.#items; 15 } 16 17 async fetchItems() { 18 ... 19 }
フロントエンドで再生するアイテムを管理するサーバー側のクラスです。
typescript:app/api/items/route.ts
1import { NextResponse } from "next/server"; 2 3import Manager from "@/app/api/Manager"; 4 5export async function GET() { 6 let manager = Manager.current; 7 return NextResponse.json(manager.getItems()); 8}
APIです。
typescript:app/page.tsx
1'use client'; 2 3import { useState, useEffect } from 'react'; 4 5import { QueueItem } from '@/app/api/types'; 6 7import Player from './components/Player'; 8import QueueList from './components/QueueList'; 9 10function App() { 11 console.log('rendering App'); 12 13 const [items, setItems] = useState<QueueItem[]>([]); 14 const [currentItemID, setCurrentItemID] = useState(''); 15 16 useEffect(() => { 17 refresh(); 18 let timer = setInterval(refresh, 10000); 19 return () => { clearInterval(timer); }; 20 // eslint-disable-next-line react-hooks/exhaustive-deps 21 }, []); 22 23 async function refresh() { 24 console.log('refresh'); 25 26 let items = await fetch(`/api/items`).then(res => res.json()) as QueueItem[]; 27 setItems(items); 28 if (currentItemID === '' && items.length > 0) { 29 setCurrentItemID(items[0].id); 30 } 31 } 32 33 return ( 34 <div className="h-screen flex bg-slate-900 text-white text-sm"> 35 <Player 36 className='grow h-full' 37 videoID={currentItemID} 38 onEnded={() => { 39 ... 40 }} 41 /> 42 <QueueList 43 items={items} 44 currentQueueID={currentItemID} 45 setQueueID={setCurrentItemID} 46 /> 47 </div> 48 ); 49} 50 51export default App;
フロントエンドです。Client Componentに指定しています。
試したこと
以下のように書き換えてみました。
typescript:app/page.tsx
1async function App() { 2 console.log('rendering App'); 3 4 // const [items, setItems] = useState<QueueItem[]>([]); 5 let items = await fetch(`/api/streams`, { next: { revalidate: 10 } }).then(res => res.json()) as QueueItem[]; 6 const [currentItemID, setCurrentItemID] = useState(''); 7 const videoID = items.find(item => item.id === currentItemID)?.media.videoID; 8 9 return ( 10 ... 11 ); 12}
サーバーにおいてレンダリングされているようですが、 TypeError: Failed to parse URL from /api/streams
というエラーが出ます。また、ブラウザ側で再描画とネットワークリクエストが無限ループします。
補足情報(FW/ツールのバージョンなど)
json
1 "dependencies": { 2 "next": "13.4.6", 3 "react": "18.2.0", 4 "react-dom": "18.2.0" 5 }

回答2件
あなたの回答
tips
プレビュー