如何配置Next.js 13 App目录中依赖useEffect的服务端组件并实现预渲染?
解决Next.js 13 App目录中服务端组件获取数据并确保预渲染的问题
你现在的问题核心是混淆了服务端组件(Server Component)和客户端组件(Client Component)的职责——服务端组件根本不需要用useEffect来获取数据,它可以直接在服务器上执行异步代码,完成数据获取和渲染后再把HTML发送给客户端。下面我给你两种解决方案,分别对应不同的数据量场景:
方案一:服务端获取全量数据,客户端处理分页交互
这种方案适合数据量不大的情况,一次性把所有数据拿到服务端,再交给客户端组件处理分页切换的交互:
1. 改造主组件为服务端组件
把原来的Newsletters改成服务端组件,直接在顶层用async/await获取数据,不需要useState和useEffect:
import Navbar from '@/components/navbar' import Footer from '@/components/footer' import NewsletterClient from './newsletter-client'; import axios from 'axios'; // 默认就是服务端组件,无需添加"use client" const Newsletters = async () => { // 直接在服务端发起请求,数据获取完成后再渲染 const res = await axios.get(`${process.env.NEXT_PUBLIC_BACKEND_API}/newsletters`); const allIssues = res.data; return ( <> <Navbar /> <div className="newsletter-container" id='newsletter-container'> <h1>Newsletters</h1> <hr></hr> {/* 将全量数据传给客户端组件处理交互 */} <NewsletterClient allIssues={allIssues} /> </div> <Footer /> </> ); } export default Newsletters;
2. 创建客户端组件处理交互
新建newsletter-client.js,专门处理分页状态和点击事件,必须添加'use client'标识:
'use client'; // 标记为客户端组件,支持React状态和事件 import Pagination from './pagination'; import IssueCards from './issueCards'; import { useState } from 'react'; const NewsletterClient = ({ allIssues }) => { const [currentPage, setCurrentPage] = useState(1); const issuesPerPage = 5; // 可根据需求调整,也可以作为props传入 // 切换页码的逻辑 const paginate = (pageNumber) => setCurrentPage(pageNumber); // 计算当前页需要展示的数据 const indexOfLastIssue = currentPage * issuesPerPage; const indexOfFirstIssue = indexOfLastIssue - issuesPerPage; const currentIssues = allIssues.slice(indexOfFirstIssue, indexOfLastIssue); return ( <div className="newsletter-wrapper"> <IssueCards issues={currentIssues} /> <Pagination issuesPerPage={issuesPerPage} totalIssues={allIssues.length} paginate={paginate} /> </div> ); }; export default NewsletterClient;
方案二:服务端分页(适合大数据量场景)
如果你的新闻通讯数据很多,一次性加载全量数据会影响性能,可以用URL搜索参数实现服务端分页,每次切换页码都从服务端获取对应页的数据:
1. 服务端组件根据搜索参数获取数据
import Navbar from '@/components/navbar' import Footer from '@/components/footer' import Pagination from './pagination'; import IssueCards from './issueCards'; import axios from 'axios'; // 接收searchParams参数,获取URL中的page参数 const Newsletters = async ({ searchParams }) => { const currentPage = parseInt(searchParams.page) || 1; const issuesPerPage = 5; // 向后端请求对应页的数据(假设你的API支持page和limit参数) const res = await axios.get(`${process.env.NEXT_PUBLIC_BACKEND_API}/newsletters`, { params: { page: currentPage, limit: issuesPerPage } }); const { issues, totalIssues } = res.data; // 假设后端返回当前页数据和总条数 return ( <> <Navbar /> <div className="newsletter-container" id='newsletter-container'> <h1>Newsletters</h1> <hr></hr> <div className="newsletter-wrapper"> <IssueCards issues={issues} /> {/* 把当前页、总条数等参数传给分页组件 */} <Pagination currentPage={currentPage} issuesPerPage={issuesPerPage} totalIssues={totalIssues} /> </div> </div> <Footer /> </> ); } export default Newsletters;
2. 客户端分页组件处理URL跳转
修改Pagination组件为客户端组件,通过修改URL的搜索参数触发服务端重新渲染:
'use client'; import { useRouter, useSearchParams } from 'next/navigation'; const Pagination = ({ currentPage, issuesPerPage, totalIssues }) => { const router = useRouter(); const searchParams = useSearchParams(); const paginate = (pageNumber) => { const newSearchParams = new URLSearchParams(searchParams); newSearchParams.set('page', pageNumber); // 修改URL,触发服务端重新获取对应页的数据 router.push(`/newsletters?${newSearchParams.toString()}`); }; // 生成页码数组 const pageNumbers = []; for (let i = 1; i <= Math.ceil(totalIssues / issuesPerPage); i++) { pageNumbers.push(i); } return ( <nav> <ul className="pagination"> {pageNumbers.map(number => ( <li key={number} className={currentPage === number ? 'active' : ''}> <button onClick={() => paginate(number)}> {number} </button> </li> ))} </ul> </nav> ); }; export default Pagination;
关键知识点总结
- 服务端组件默认无需标识:Next.js 13 App目录下的组件默认都是服务端组件,不需要
'use client',可以直接在顶层使用async/await获取数据,数据获取完成后才会渲染HTML发送给客户端,确保内容预渲染完成。 - 拆分职责:服务端组件负责数据获取和静态内容渲染,客户端组件负责交互逻辑(状态管理、事件处理),这样既保证了SEO和首屏加载速度,又能实现交互功能。
- 环境变量使用:
NEXT_PUBLIC_前缀的环境变量在服务端和客户端都能访问,如果是服务端专用的变量(不带前缀),也可以在服务端组件中直接使用。
内容的提问来源于stack exchange,提问作者SteveMaet
相关产品推荐
相关产品推荐

