Next.js中如何从子组件更新状态实现评论无刷新更新?
问题
在我的Next.js应用中,我使用getServerSideProps为[url].tsx路由从数据库拉取数据(url对应数据库中的特定项目),并通过{project.name}等填充页面内容。页面下方有两个区域:添加评论区(含提交表单子组件)和查看评论区(评论存储在包含评分、名称、内容、关联项目ID的表中,含评论统计子组件,显示评论总数和项目平均评分)。
我希望在提交评论后无需刷新页面,就能更新平均评分并显示新评论。我尝试使用React的useState初始化状态:
const [comments, setComments] = useState(project.comments)
但因组件拆分,我在CommentForm组件提交数据到API路由后,不清楚如何将setComments传递给CommentStats和CommentGrid组件,进而更新状态,这让我感到困惑。
附上相关组件代码:
CommentStats.tsx
type CommentStatsProps = { comments: any; } const CommentStats = ({ comments }: CommentStatsProps) => { const totalComments = comments.length; const totalStars = Comments.reduce((acc: any, curr: { rating: any; }) => acc + curr.rating, 0); const averageStars = comments / totalComments; return ( <div className="max-w-4xl mx-auto flex justify-between items-center p-8 my-12 bg-white rounded-lg shadow-lg"> <div className="flex items-center p-4 bg-violet-500 rounded-lg space-x-4"> <h1 className="text-4xl font-bold text-white">{averageStars.toFixed(1)}</h1> <FaStar className="h-5 text-3xl text-yellow-300" /> </div> <div className="flex items-center p-4 bg-slate-500 rounded-lg space-x-4"> <p className="text-2xl text-white"> <span className="text-4xl font-bold">{totalComments}</span> {totalComments === 1 ? "Comment" : "Comments"} </p> </div> </div> ); } export default CommentStats
CommentGrid.tsx
import CommentCard from "@/components/Comments/CommentCard"; /* eslint-disable @typescript-eslint/no-explicit-any */ type CommentsProps = { comments: any; } type CommentProps = { id: string; name: string; comment: string; rating: number; createdAt: string; } const CommentGrid = ({ comments }: CommentsProps) => { const totalComments = comments.length; const left = comments.slice(0, Math.ceil(totalComments / 2)); const right = comments.slice(Math.ceil(totalComments / 2), totalComments); const list = { visible: { opacity: 1, y: 0, transition: { when: "beforeChildren", staggerChildren: 0.5 } }, hidden: { opacity: 0, y: -100 } } const item = { visible: { opacity: 1, y: 0 }, hidden: { opacity: 0, y: -100 } } // Check if there are any comments, if not return a message if (totalComments === 0) { return ( <div className="tracking-wider text-center"> <h1 className="text-4xl pb-4">No Comments Yet</h1> <p className="text-2xl">Be the first to leave a comment!</p> </div> ) } return ( <motion.div // initial={{ opacity: 0 }} // animate={{ opacity: 1 }} // whileInView={{ opacity: 1 }} // transition={{ duration: 0.5 }} // viewport={{ once: true}} className="grid md:grid-cols-2 gap-8"> {/* Left */} <motion.ul whileInView={{ opacity: 1 }} initial="hidden" animate="visible" variants={list} className="space-y-8"> {left.map((comment: CommentProps) => ( <motion.li variants={item} key={comment.id} > <CommentCard stars={comment.rating} author={comment.name} comment={comment.comment} date={comment.createdAt} /> </motion.li> ))} </motion.ul> </motion.div> ) } export default CommentGrid
我不想把实现复杂化,但希望在API后台处理评论创建时实现实时更新,请问有没有简单的解决方法?
简单解决方案
1. 将状态提升到父组件([url].tsx)
把comments状态和setComments放在最顶层的页面组件中,将comments传递给CommentStats和CommentGrid,同时给CommentForm传递回调函数,用于提交成功后更新状态。
示例父组件代码:
// [url].tsx import { useState } from 'react'; import CommentForm from '@/components/Comments/CommentForm'; import CommentStats from '@/components/Comments/CommentStats'; import CommentGrid from '@/components/Comments/CommentGrid'; export async function getServerSideProps(context) { // 原有数据拉取逻辑,获取project及关联评论 const project = await fetchProjectByUrl(context.params.url); return { props: { project } }; } export default function ProjectPage({ project }) { const [comments, setComments] = useState(project.comments); // 定义提交回调,传给CommentForm const handleCommentSubmit = async (newCommentData) => { try { // 调用API提交评论 const res = await fetch('/api/comments', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...newCommentData, projectId: project.id }) }); if (res.ok) { // 获取API返回的完整新评论(含数据库生成的id、createdAt等) const newComment = await res.json(); // 将新评论添加到列表头部,更新本地状态 setComments(prev => [newComment, ...prev]); } } catch (err) { console.error('提交评论失败:', err); } }; return ( <div> <h1>{project.name}</h1> {/* 其他项目内容 */} {/* 添加评论区 */} <CommentForm onSubmit={handleCommentSubmit} /> {/* 评论统计区 */} <CommentStats comments={comments} /> {/* 评论列表区 */} <CommentGrid comments={comments} /> </div> ); }
2. 修改CommentForm接收并调用回调
在CommentForm中,表单提交成功后调用父组件传入的onSubmit回调,传递新评论数据:
// CommentForm.tsx import { useState } from 'react'; export default function CommentForm({ onSubmit }) { const [name, setName] = useState(''); const [comment, setComment] = useState(''); const [rating, setRating] = useState(5); const handleFormSubmit = async (e) => { e.preventDefault(); // 调用父组件回调,传递表单数据 await onSubmit({ name, comment, rating }); // 重置表单 setName(''); setComment(''); setRating(5); }; return ( <form onSubmit={handleFormSubmit} className="space-y-4"> <input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="你的名字" required className="w-full p-2 border rounded" /> <textarea value={comment} onChange={(e) => setComment(e.target.value)} placeholder="写下你的评论" required rows={4} className="w-full p-2 border rounded" /> <div className="flex space-x-2"> {[1,2,3,4,5].map(num => ( <button type="button" onClick={() => setRating(num)} className={`px-3 py-1 rounded ${rating === num ? 'bg-yellow-400 text-white' : 'bg-gray-100'}`} key={num} > {num}星 </button> ))} </div> <button type="submit" className="w-full p-3 bg-violet-500 text-white rounded"> 提交评论 </button> </form> ); }
3. 修复CommentStats的代码错误
原组件存在两处逻辑错误,修正后才能正确计算平均评分:
// CommentStats.tsx import { FaStar } from 'react-icons/fa'; type CommentStatsProps = { comments: Array<{ rating: number }>; } const CommentStats = ({ comments }: CommentStatsProps) => { const totalComments = comments.length; // 修正:将Comments改为comments(小写开头) const totalStars = comments.reduce((acc: number, curr) => acc + curr.rating, 0); // 修正:用totalStars除以totalComments,处理0评论避免NaN const averageStars = totalComments > 0 ? totalStars / totalComments : 0; return ( <div className="max-w-4xl mx-auto flex justify-between items-center p-8 my-12 bg-white rounded-lg shadow-lg"> <div className="flex items-center p-4 bg-violet-500 rounded-lg space-x-4"> <h1 className="text-4xl font-bold text-white">{averageStars.toFixed(1)}</h1> <FaStar className="h-5 text-3xl text-yellow-300" /> </div> <div className="flex items-center p-4 bg-slate-500 rounded-lg space-x-4"> <p className="text-2xl text-white"> <span className="text-4xl font-bold">{totalComments}</span> {totalComments === 1 ? "Comment" : "Comments"} </p> </div> </div> ); } export default CommentStats
方案优势
- 无需引入复杂状态管理库,仅用React内置API实现
- 组件职责清晰:父组件管理状态,子组件专注展示和交互
- 提交成功后直接更新本地状态,无需重新拉取整个评论列表,性能更优
内容的提问来源于stack exchange,提问作者Louis Stephens
相关产品推荐
相关产品推荐

