質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.31%
Next.js

Next.jsは、Reactを用いたサーバサイドレンダリングなどを行う軽量なフレームワークです。Zeit社が開発しており、nextコマンドでプロジェクトを作成することにより、開発環境整備が整った環境が即時に作成できます。

Q&A

解決済

2回答

2054閲覧

Next.js 13のApp Routerでサーバー側で最新の情報を用いてHTMLをレンダリングし、かつポーリングして表示更新する

yuntan

総合スコア1

Next.js

Next.jsは、Reactを用いたサーバサイドレンダリングなどを行う軽量なフレームワークです。Zeit社が開発しており、nextコマンドでプロジェクトを作成することにより、開発環境整備が整った環境が即時に作成できます。

0グッド

0クリップ

投稿2023/06/22 02:45

編集2023/06/22 05:27

回答を迅速に得るため、StackOverflowとのクロスポストを行っております。
https://ja.stackoverflow.com/questions/95322

実現したいこと

  1. ページ単位で状態を共有する(実装済みだが、Next.jsの枝葉をClient Componentにして幹はServer Componentにするという原則に反してしまっているので、できれば解消したい)。
  2. ブラウザでページロード時に即座に最新の情報が表示されるようにするため、サーバー側で最新の情報を用いてHTMLをレンダリングする(必ずしも必要な機能ではないが、技術的チャレンジとして)。
  3. 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 }

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

1T2R3M4

2023/06/22 04:13

https://ja.stackoverflow.com/questions/95322 やむを得ず複数のサイトに質問を投稿された場合は、質問内容にマルチポストをする理由を書き、他のサイトの投稿へのリンクを貼ってください。
guest

回答2

0

自己解決

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

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

typescript:app/page.tsx

1async function Page() { 2 return <App />; 3} 4 5export default Page;

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

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

typescript:app/page.tsx

1async function Page() { 2 console.log('rendering Page'); 3 4 const streams = await getLiveStreamItems(); 5 const videos = await getVideoItems(); 6 7 return ( 8 <App 9 initialStreams={streams} 10 /> 11 ); 12} 13 14export default Page;

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

SWRというライブラリを使う。

typescript:app/components/App.tsx

1'use client'; 2 3import { useState } from 'react'; 4import useSWR from 'swr'; 5import type { SWRConfiguration } from 'swr'; 6 7import fetcher from '@/app/utils/fetcher'; 8 9import Player from './Player'; 10import List from './List'; 11 12type Props = { 13 initialStreams: QueueItem[]; 14}; 15 16function App(props: Props) { 17 const { initialStreams } = props; 18 const [currentItemID, setCurrentItemID] = useState(''); 19 20 // https://swr.vercel.app/ja/docs/typescript#generics 21 let config: SWRConfiguration = { 22 refreshInterval: 60_000, // 1 minute 23 fallbackData: initialStreams, 24 }; 25 let { data: streams } = useSWR<QueueItem>('/api/streams', fetcher, config); 26 streams = streams ?? []; 27 28 return ( 29 <div> 30 <div className="flex"> 31 <Player 32 className='grow' 33 videoID={videoID} 34 onEnded={() => {...}} 35 /> 36 </div> 37 <List 38 className='m-4 mx-auto' 39 streamItems={streams} 40 currentItemID={currentItemID} 41 setItemID={setCurrentItemID} 42 /> 43 </div> 44 ); 45} 46 47export default App;

投稿2023/07/04 21:21

yuntan

総合スコア1

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

この回答はGPT-4による自動生成回答です

Next.js 13では、Server ComponentとClient Componentを組み合わせることができます。Server Componentでデータを取得し、Client Componentでデータを更新する方法を提案します。

まず、Server Componentでデータを取得します。

typescript

1// app/page.server.tsx 2import { QueueItem } from '@/app/api/types'; 3import Manager from '@/app/api/Manager'; 4 5export async function getServerComponentProps() { 6 const manager = Manager.current; 7 const items = manager.getItems(); 8 return { items }; 9}

次に、Client Componentでデータを更新します。

typescript

1// app/page.client.tsx 2import { useState, useEffect } from 'react'; 3import { QueueItem } from '@/app/api/types'; 4 5import Player from './components/Player'; 6import QueueList from './components/QueueList'; 7 8function App({ items: initialItems }: { items: QueueItem[] }) { 9 const [items, setItems] = useState<QueueItem[]>(initialItems); 10 const [currentItemID, setCurrentItemID] = useState(''); 11 12 useEffect(() => { 13 refresh(); 14 let timer = setInterval(refresh, 10000); 15 return () => { clearInterval(timer); }; 16 // eslint-disable-next-line react-hooks/exhaustive-deps 17 }, []); 18 19 async function refresh() { 20 let items = await fetch(`/api/items`).then(res => res.json()) as QueueItem[]; 21 setItems(items); 22 if (currentItemID === '' && items.length > 0) { 23 setCurrentItemID(items[0].id); 24 } 25 } 26 27 return ( 28 <div className="h-screen flex bg-slate-900 text-white text-sm"> 29 <Player 30 className='grow h-full' 31 videoID={currentItemID} 32 onEnded={() => { 33 ... 34 }} 35 /> 36 <QueueList 37 items={items} 38 currentQueueID={currentItemID} 39 setQueueID={setCurrentItemID} 40 /> 41 </div> 42 ); 43} 44 45export default App;

最後に、app/page.tsxでServer ComponentとClient Componentを組み合わせます。

typescript

1// app/page.tsx 2import type { ServerComponentProps } from './page.server'; 3import App from './page.client'; 4 5export default function Page(props: ServerComponentProps) { 6 return <App {...props} />; 7}

これで、ページロード時にサーバー側で最新の情報を用いてHTMLをレンダリングし、APIをポーリングして表示を更新することができます。

投稿2023/06/22 02:46

teratail-ai

Powered by GPT-4

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

yuntan

2023/06/22 03:01

page.server.tsxから `ServerComponentProps` をインポートできません。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.31%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問