Next.js v14 TypeScript页面集成JSON API及动态路由实现问题
问题1:React钩子导入错误与API集成
原因
Next.js 14默认使用App Router,你的Home组件属于服务器组件,这类组件不支持使用useEffect、useState等React钩子,这就是导入报错的核心原因。
解决方案
提供两种适配场景的实现方式:
方式1:转为客户端组件(适合前端动态渲染场景)
在组件最顶部添加'use client'指令,将组件转为客户端组件后即可正常使用钩子:
'use client'; // 必须放在代码最开头 import Image from 'next/image'; import type { Metadata } from 'next'; import { Montserrat } from 'next/font/google'; import { useEffect, useState } from 'react'; // 现在可正常导入 import './../assets/css/globals.css'; import './../assets/libs/devicon/devicon.min.css'; // 定义API返回的Event类型(根据实际接口结构补充字段) interface Event { id: string; title: string; start_date: string; } export const metadata: Metadata = { title: 'Events List', description: 'Browse trucking events', }; // 字体定义保持不变 const montserrat_thin = Montserrat({ weight: ['100'], style: ['normal'], subsets: ['latin'] }); const montserrat_regular = Montserrat({ weight: ['400'], style: ['normal'], subsets: ['latin'] }); const montserrat_semiblack = Montserrat({ weight: ['700'], style: ['normal'], subsets: ['latin'] }); const montserrat_black = Montserrat({ weight: ['900'], style: ['normal'], subsets: ['latin'] }); export default function Home() { const [events, setEvents] = useState<Event[]>([]); const [loading, setLoading] = useState(true); useEffect(() => { const fetchEvents = async () => { try { const res = await fetch('https://api.truckyapp.com/v2/events'); if (!res.ok) throw new Error('Failed to load events'); const data = await res.json(); setEvents(data); } catch (err) { console.error('获取数据失败:', err); } finally { setLoading(false); } }; fetchEvents(); }, []); if (loading) return <div className="text-center py-8">加载中...</div>; return ( <main className={montserrat_regular.className}> {/* 原有页面结构保留 */} <div className="flex gap-x-8 gap-y-8 skewY-0 mx-auto flex max-w-7xl"> <div className="relative p-5"> <span className={`${montserrat_thin.className} prenom`}>Test</span> <span className={`${montserrat_black.className} nom`}>Test</span> </div> </div> <div className="mateo-grid bg-mateo-unique h-[100%]"> <div className="skewY-0 mx-auto max-w-7xl top-[-7rem] padding-right relative"> <div className="w-full columns-1"> <h1 className={`${montserrat_black.className} titre`}>Trucking Events</h1> </div> <div className="w-full flex grid p-2 grid-cols-1 sm:grid-cols-4 lg:grid-cols-4"> {/* 渲染事件列表 */} {events.map(event => ( <div key={event.id} className="p-4 border rounded mb-4"> <h3 className={montserrat_semiblack.className}>{event.title}</h3> <p className="text-sm mt-2">开始时间: {event.start_date}</p> {/* 链接到详情页 */} <a href={`/${event.id}`} className="mt-3 inline-block text-blue-600">查看详情</a> </div> ))} </div> </div> </div> </main> ) }
方式2:保持服务器组件(适合静态预渲染/缓存场景)
服务器组件可直接使用fetch且无需钩子,Next.js会自动处理请求缓存:
import Image from 'next/image'; import type { Metadata } from 'next'; import { Montserrat } from 'next/font/google'; import './../assets/css/globals.css'; import './../assets/libs/devicon/devicon.min.css'; interface Event { id: string; title: string; start_date: string; } export const metadata: Metadata = { title: 'Events List', description: 'Browse trucking events', }; // 字体定义保持不变 const montserrat_thin = Montserrat({ weight: ['100'], style: ['normal'], subsets: ['latin'] }); const montserrat_regular = Montserrat({ weight: ['400'], style: ['normal'], subsets: ['latin'] }); const montserrat_semiblack = Montserrat({ weight: ['700'], style: ['normal'], subsets: ['latin'] }); const montserrat_black = Montserrat({ weight: ['900'], style: ['normal'], subsets: ['latin'] }); // 服务器组件中直接异步获取数据 async function getEvents() { const res = await fetch('https://api.truckyapp.com/v2/events', { next: { revalidate: 60 } }); // 每60秒刷新缓存 if (!res.ok) throw new Error('Failed to load events'); return res.json() as Promise<Event[]>; } export default async function Home() { const events = await getEvents(); return ( <main className={montserrat_regular.className}> {/* 原有页面结构保留 */} <div className="flex gap-x-8 gap-y-8 skewY-0 mx-auto flex max-w-7xl"> <div className="relative p-5"> <span className={`${montserrat_thin.className} prenom`}>Test</span> <span className={`${montserrat_black.className} nom`}>Test</span> </div> </div> <div className="mateo-grid bg-mateo-unique h-[100%]"> <div className="skewY-0 mx-auto max-w-7xl top-[-7rem] padding-right relative"> <div className="w-full columns-1"> <h1 className={`${montserrat_black.className} titre`}>Trucking Events</h1> </div> <div className="w-full flex grid p-2 grid-cols-1 sm:grid-cols-4 lg:grid-cols-4"> {events.map(event => ( <div key={event.id} className="p-4 border rounded mb-4"> <h3 className={montserrat_semiblack.className}>{event.title}</h3> <p className="text-sm mt-2">开始时间: {event.start_date}</p> <a href={`/${event.id}`} className="mt-3 inline-block text-blue-600">查看详情</a> </div> ))} </div> </div> </div> </main> ) }
问题2:动态路由展示单个Event详情
步骤1:创建动态路由页面
在app目录下创建[slug_id]文件夹,内部新建page.tsx文件:
// app/[slug_id]/page.tsx import { Montserrat } from 'next/font/google'; import type { Metadata } from 'next'; // 定义Event详情类型(根据实际接口结构补充字段) interface EventDetail { id: string; title: string; description: string; start_date: string; location: string; } const montserrat_regular = Montserrat({ weight: ['400'], subsets: ['latin'] }); const montserrat_black = Montserrat({ weight: ['900'], subsets: ['latin'] }); // 生成页面元数据(可选,优化SEO) export async function generateMetadata({ params }: { params: { slug_id: string } }) { const event = await getEventDetail(params.slug_id); return { title: event.title, description: event.description || 'Event Detail', }; } // 获取单个Event详情 async function getEventDetail(eventId: string) { const res = await fetch(`https://api.truckyapp.com/v2/events/${eventId}`, { next: { revalidate: 60 } }); if (!res.ok) throw new Error('Failed to load event detail'); return res.json() as Promise<EventDetail>; } export default async function EventDetailPage({ params }: { params: { slug_id: string } }) { const event = await getEventDetail(params.slug_id); return ( <main className={montserrat_regular.className}> <div className="max-w-4xl mx-auto p-8"> <h1 className={montserrat_black.className}>{event.title}</h1> <div className="mt-4 space-y-4"> <p><strong>开始时间:</strong> {event.start_date}</p> <p><strong>地点:</strong> {event.location}</p> <div className="mt-6"> <h3 className="text-lg font-semibold">活动描述</h3> <p className="mt-2">{event.description}</p> </div> </div> <a href="/" className="mt-6 inline-block text-blue-600">返回活动列表</a> </div> </main> ); }
关键说明
- 动态路由通过
[slug_id]命名文件夹实现,Next.js会自动解析URL中的slug_id参数并传入组件的params对象。 - 如果需要在详情页添加交互逻辑,可在文件顶部添加
'use client'转为客户端组件,使用useParams()获取slug_id后再通过useEffect获取数据。 - 请确认API的单个Event端点格式,若实际接口路径不是
/events/{id},需对应调整fetch的URL。
内容的提问来源于stack exchange,提问作者Mateo
相关产品推荐
相关产品推荐

