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

Next.js 14客户端组件调用Server Actions时async/await报错求助

在Next.js 14.0.1中客户端组件调用Server Action实现无限滚动的解决方案

问题说明

在Next.js 14.0.1构建无限滚动功能时,客户端组件内直接通过async/await调用Server Action(getData)触发以下错误:

"async/await is not yet supported in Client Components, only Server Components."

服务端组件page.tsx可正常调用该Server Action获取初始数据,但客户端组件InfiniteScrollData.tsx调用时出错。

现有代码片段

page.tsx(服务端组件)

// page.tsx
const Home = async ({ searchParams }) => {
  const searchKeyword =
    typeof searchParams.keyword === "string" ? searchParams.keyword : "";
  
  let apiResponse = await getPhotos({ query: searchKeyword });

  return (
    <main className='p-6 flex justify-center w-full min-h-screen bg-black'>
      <InfiniteScrollData
        search={searchKeyword}
        initialData={apiResponse?.results || []}
      />
    </main>
  );
};
export default Home;

actions.ts(Server Action)

// actions.ts
"use server";

export const getData = async ({
  query,
}) => {
  
  const { response } = await fetchData({
    query,
  });
  apiResponse = response;
  
  return response;
};

InfiniteScrollData.tsx(客户端组件)

// InfiniteScrollData.tsx
"use client";

// imports

const InfiniteScrollImages = ({
  search,
  initialData,
}) => {
  const [data, setData] = useState(initialData);
  const [page, setPage] = useState(1);
  const [loaderRef, inView] = useInView();

  const loadMoreData = useCallback(async () => {
    const next = page + 1;
    const response = await getData({ query: search, page: next });
    if (response?.results?.length) {
      setPage(next);
      setData((prev) => [...prev, ...(response?.results || [])]);
    }
  }, [page, search]);

  useEffect(() => {
    if (inView) {
      loadMoreData();
    }
  }, [inView, loadMoreData]);

  return (
    <>
      <div>
        {data?.map((item) => (
          <Card key={item.id} data={item} />
        ))}
      </div>
      <div ref={loaderRef}>
        <Loader />
      </div>
    </>
  );
};
export default InfiniteScrollImages;

可行解决方案

1. 修正客户端组件的异步调用逻辑

错误的核心是不能在组件渲染阶段使用async/await,但在useCallback、useEffect这类回调函数中调用异步Server Action是完全允许的。只需给现有代码添加加载状态锁和错误处理即可:

"use client";

import { useState, useCallback, useEffect } from 'react';
import { getData } from './actions';

const InfiniteScrollImages = ({
  search,
  initialData,
}) => {
  const [data, setData] = useState(initialData);
  const [page, setPage] = useState(1);
  const [isLoading, setIsLoading] = useState(false); // 新增加载状态,防止重复请求
  const [loaderRef, inView] = useInView();

  const loadMoreData = useCallback(async () => {
    if (isLoading) return; // 加载中时拒绝新请求
    setIsLoading(true);
    
    try {
      const nextPage = page + 1;
      const response = await getData({ query: search, page: nextPage });
      
      if (response?.results?.length) {
        setPage(nextPage);
        setData(prev => [...prev, ...response.results]);
      }
    } catch (error) {
      console.error('加载更多数据失败:', error);
      // 这里可以添加前端错误提示逻辑
    } finally {
      setIsLoading(false); // 无论成功失败都重置加载状态
    }
  }, [page, search, isLoading]);

  useEffect(() => {
    if (inView && !isLoading) {
      loadMoreData();
    }
  }, [inView, loadMoreData, isLoading]);

  return (
    <>
      <div>
        {data?.map(item => (
          <Card key={item.id} data={item} />
        ))}
      </div>
      {isLoading && (
        <div ref={loaderRef}>
          <Loader />
        </div>
      )}
    </>
  );
};
export default InfiniteScrollImages;

2. 用startTransition优化UI体验

如果加载数据时不想阻塞用户交互,可以用React的startTransition包裹状态更新,标记这是一个非紧急更新:

import { startTransition } from 'react';

// ... 其他代码不变,修改loadMoreData中的状态更新部分
const loadMoreData = useCallback(async () => {
  if (isLoading) return;
  setIsLoading(true);
  
  try {
    const nextPage = page + 1;
    const response = await getData({ query: search, page: nextPage });
    
    if (response?.results?.length) {
      startTransition(() => {
        setPage(nextPage);
        setData(prev => [...prev, ...response.results]);
      });
    }
  } catch (error) {
    console.error('加载更多数据失败:', error);
  } finally {
    setIsLoading(false);
  }
}, [page, search, isLoading]);

3. 备选:通过API路由调用Server Action

如果你更习惯传统API调用模式,可以把Server Action封装为API路由,再用fetch在客户端调用:

步骤1:创建API路由

// app/api/get-photos/route.ts
import { getData } from '@/actions';
import { NextResponse } from 'next/server';

export async function POST(request: Request) {
  const { query, page } = await request.json();
  const result = await getData({ query, page });
  return NextResponse.json(result);
}

步骤2:客户端组件中用fetch调用

const loadMoreData = useCallback(async () => {
  if (isLoading) return;
  setIsLoading(true);
  
  try {
    const nextPage = page + 1;
    const res = await fetch('/api/get-photos', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ query: search, page: nextPage }),
    });
    
    if (!res.ok) throw new Error('请求失败');
    const response = await res.json();
    
    if (response?.results?.length) {
      setPage(nextPage);
      setData(prev => [...prev, ...response.results]);
    }
  } catch (error) {
    console.error('加载更多数据失败:', error);
  } finally {
    setIsLoading(false);
  }
}, [page, search, isLoading]);

内容的提问来源于stack exchange,提问作者Haider Ali Anjum

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 11:44:53