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

Next.js客户端页面调用异步服务端组件传参报错,求解决方案

问题分析与解决方案

核心问题是标记了"use client"的组件中不能直接渲染异步组件(带async/await的组件)——Next.js规定异步组件默认是Server Component,无法在Client Component中直接使用,这就是你点击按钮时报错的原因。

不需要为每个宝可梦世代单独创建页面,以下是几种适配你需求的实现方案:

方案1:客户端组件内用useEffect或数据获取库处理异步请求

把CardList和Card改成普通客户端组件,将数据获取逻辑移到Home页面(use client)中,通过状态变化触发请求:

// app/page.tsx ("use client")
'use client';

import { useState, useEffect } from 'react';
import CardList from './CardList';

export default function Home() {
  const [offset, setOffset] = useState(0);
  const [limit, setLimit] = useState(20);
  const [pokemons, setPokemons] = useState<any[]>([]);
  const [loading, setLoading] = useState(false);

  // 监听offset/limit变化,触发数据请求
  useEffect(() => {
    const fetchPokemons = async () => {
      setLoading(true);
      try {
        const res = await fetch(`https://pokeapi.co/api/v2/pokemon?offset=${offset}&limit=${limit}`);
        const data = await res.json();
        // 可选:进一步获取每个宝可梦的详情
        const detailPromises = data.results.map(async (p: any) => {
          const detailRes = await fetch(p.url);
          return detailRes.json();
        });
        const pokemonDetails = await Promise.all(detailPromises);
        setPokemons(pokemonDetails);
      } catch (err) {
        console.error('获取数据失败:', err);
      } finally {
        setLoading(false);
      }
    };

    fetchPokemons();
  }, [offset, limit]);

  return (
    <div>
      <button onClick={() => {setOffset(0); setLimit(151);}}>Gen 1</button>
      <button onClick={() => {setOffset(151); setLimit(100);}}>Gen 2-3</button>
      {loading ? <p>加载中...</p> : <CardList pokemons={pokemons} />}
    </div>
  );
}

// app/CardList.tsx (普通客户端组件,无需async)
export default function CardList({ pokemons }: { pokemons: any[] }) {
  return (
    <div className="grid grid-cols-4 gap-4">
      {pokemons.map(pokemon => (
        <Card key={pokemon.id} pokemon={pokemon} />
      ))}
    </div>
  );
}

如果后续要处理缓存、重复请求、自动重新获取等场景,推荐用SWR或React Query替代useEffect,它们能更优雅地管理客户端数据。

方案2:通过URL搜索参数触发Server Component数据更新

保留CardList为Server Component,通过客户端组件修改URL的搜索参数,让Server Component读取参数并获取数据:

// app/page.tsx ("use client")
'use client';

import { useRouter, useSearchParams } from 'next/navigation';

export default function Home() {
  const router = useRouter();
  const searchParams = useSearchParams();

  const handleGenClick = (offset: number, limit: number) => {
    const params = new URLSearchParams(searchParams);
    params.set('offset', offset.toString());
    params.set('limit', limit.toString());
    router.push(`/?${params.toString()}`);
  };

  return (
    <div>
      <button onClick={() => handleGenClick(0, 151)}>Gen 1</button>
      <button onClick={() => handleGenClick(151, 100)}>Gen 2-3</button>
      {/* Server Component会自动根据URL参数更新 */}
      <CardList />
    </div>
  );
}

// app/CardList.tsx (Server Component,无需"use client")
export default async function CardList({ searchParams }: { searchParams: { offset?: string; limit?: string } }) {
  const offset = parseInt(searchParams.offset || '0');
  const limit = parseInt(searchParams.limit || '20');

  const res = await fetch(`https://pokeapi.co/api/v2/pokemon?offset=${offset}&limit=${limit}`);
  const data = await res.json();
  const pokemonDetails = await Promise.all(
    data.results.map(async (p: any) => {
      const detailRes = await fetch(p.url);
      return detailRes.json();
    })
  );

  return (
    <div className="grid grid-cols-4 gap-4">
      {pokemonDetails.map(pokemon => (
        <Card key={pokemon.id} pokemon={pokemon} />
      ))}
    </div>
  );
}

这种方案的优势是数据在服务端获取,对SEO更友好,同时客户端通过URL状态实现交互,适合公开的内容展示。

方案3:用Server Actions处理服务端数据获取

如果后续需要添加用户功能(比如鉴权、用户专属数据),可以用Next.js的Server Actions在服务端完成数据请求,客户端组件调用Action即可:

// app/actions.ts (Server Action,无需"use client")
'use server';

export async function fetchPokemons(offset: number, limit: number) {
  // 这里可以添加用户鉴权逻辑,比如从session获取用户信息
  const res = await fetch(`https://pokeapi.co/api/v2/pokemon?offset=${offset}&limit=${limit}`);
  const data = await res.json();
  const pokemonDetails = await Promise.all(
    data.results.map(async (p: any) => {
      const detailRes = await fetch(p.url);
      return detailRes.json();
    })
  );
  return pokemonDetails;
}

// app/page.tsx ("use client")
'use client';

import { useState } from 'react';
import { fetchPokemons } from './actions';
import CardList from './CardList';

export default function Home() {
  const [offset, setOffset] = useState(0);
  const [limit, setLimit] = useState(20);
  const [pokemons, setPokemons] = useState<any[]>([]);
  const [loading, setLoading] = useState(false);

  const handleGenClick = async (newOffset: number, newLimit: number) => {
    setLoading(true);
    try {
      const data = await fetchPokemons(newOffset, newLimit);
      setPokemons(data);
      setOffset(newOffset);
      setLimit(newLimit);
    } catch (err) {
      console.error('获取数据失败:', err);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div>
      <button onClick={() => handleGenClick(0, 151)}>Gen 1</button>
      <button onClick={() => handleGenClick(151, 100)}>Gen 2-3</button>
      {loading ? <p>加载中...</p> : <CardList pokemons={pokemons} />}
    </div>
  );
}

Server Actions的逻辑在服务端执行,适合处理需要隐私或权限控制的请求,为后续的用户功能(比如收藏宝可梦、用户个人中心)做好铺垫。


内容的提问来源于stack exchange,提问作者Sneezo

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 17:29:54