You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 05:35:01