React新闻页map中使用useModal触发Hook报错,求解决方案
解决NextUI useModal在map循环中触发的Hook错误
问题场景
开发新闻列表页面,需求是用户点击单条新闻卡片时弹出模态框展示完整内容,但在map循环中调用NextUI的useModal Hook时,触发以下错误:
Uncaught Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement in React Hooks.
错误原因
React Hook的核心规则之一:不能在循环、条件判断、嵌套函数中调用Hook。你在SortingDate(data).map(...)的回调里调用useModal,违反了这条规则——每次渲染时循环的次数可能变化,导致React无法保证Hook调用顺序的一致性,从而抛出错误。
可行解决方案
方案一:将新闻卡片抽离为独立组件
把单条新闻的渲染逻辑和模态框Hook封装到单独的组件中,让useModal在组件的顶层调用,符合Hook规则。
修改后的代码
首先修改News组件:
import SortingDate from "@/functions/Sorting"; import { useState, useEffect } from "react"; import Loading from "./loading"; import NewsCard from "./NewsCard"; // 导入新组件 function News() { const [data, setData] = useState(null); const [isLoading, setLoading] = useState(false); useEffect(() => { setLoading(true); fetch("/api/news") .then((res) => res.json()) .then((data) => { setData(data); setLoading(false); }); }, []); if (isLoading) return <Loading />; if (!data) return <Loading />; return ( <div className="py-24"> <div className="mx-auto max-w-7xl px-6 lg:px-8"> <div className="mx-auto max-w-2xl text-center"> <h2 className="text-3xl font-bold tracking-tight text-white sm:text-4xl"> Here comes the tea </h2> <p className="mt-2 text-lg leading-8 text-gray-300"> Keep in touch with the latest news. </p> </div> <div className="mx-auto mt-16 grid max-w-2xl grid-cols-1 gap-x-8 gap-y-20 lg:mx-0 lg:max-w-none lg:grid-cols-3"> {/* 循环渲染NewsCard组件 */} {SortingDate(data).map((post) => ( <NewsCard key={post.id} post={post} /> ))} </div> </div> </div> ); }
然后创建NewsCard.jsx组件:
import { useModal } from "@nextui-org/react"; import NewsModal from "@/components/ui/news-modal"; function NewsCard({ post }) { // 在组件顶层合法调用useModal const { setVisible, bindings } = useModal(); return ( <button onClick={() => setVisible((prevState) => !prevState)} className="flex flex-col items-start bg-gray-800 rounded-2xl" > <NewsModal setVisible={setVisible} bindings={bindings} id={post.id} /> <article> <div className="relative w-full"> <img src={post.imageUrl} alt="Thumbnail" className="aspect-[16/9] w-full rounded-2xl bg-gray-300 object-cover sm:aspect-[2/1] lg:aspect-[3/2]" /> <div className="absolute inset-0 rounded-2xl ring-1 ring-inset ring-gray-900/10" /> </div> <div className="max-w-xl"> <div className="mt-8 flex items-center gap-x-4 text-xs mx-2"> <time dateTime={post.datetime} className="text-gray-300"> {post.date} </time> </div> <div className="group relative mx-2"> <h3 className="mt-3 text-lg font-semibold leading-6 text-gray-300 group-hover:text-gray-500"> <a href={post.href}> <span className="absolute inset-0" /> {post.title} </a> </h3> </div> <div className="relative mt-8 flex items-center gap-x-4 mx-2 my-2"> <div className="text-sm leading-6"> <p className="font-semibold text-white"> <a> <span className="absolute inset-0" /> {post.author.name} </a> </p> </div> </div> </div> </article> </button> ); } export default NewsCard;
方案二:使用单个模态框控制显示内容
如果不想创建多个组件,可以在News组件中只初始化一个模态框,通过状态记录当前选中的新闻ID,点击时更新状态,模态框根据ID展示对应内容。这种方式更高效,避免创建多个模态框实例。
修改后的代码
import SortingDate from "@/functions/Sorting"; import { useModal } from "@nextui-org/react"; import NewsModal from "@/components/ui/news-modal"; import { useState, useEffect } from "react"; import Loading from "./loading"; function News() { const [data, setData] = useState(null); const [isLoading, setLoading] = useState(false); const [selectedPostId, setSelectedPostId] = useState(null); // 在组件顶层调用useModal,只创建一个实例 const { setVisible, bindings } = useModal(); useEffect(() => { setLoading(true); fetch("/api/news") .then((res) => res.json()) .then((data) => { setData(data); setLoading(false); }); }, []); if (isLoading) return <Loading />; if (!data) return <Loading />; // 获取当前选中的新闻详情 const selectedPost = data.find(post => post.id === selectedPostId); return ( <div className="py-24"> <div className="mx-auto max-w-7xl px-6 lg:px-8"> <div className="mx-auto max-w-2xl text-center"> <h2 className="text-3xl font-bold tracking-tight text-white sm:text-4xl"> Here comes the tea </h2> <p className="mt-2 text-lg leading-8 text-gray-300"> Keep in touch with the latest news. </p> </div> {/* 单个模态框,根据selectedPost展示内容 */} <NewsModal setVisible={setVisible} bindings={bindings} post={selectedPost} // 传递选中的新闻数据 /> <div className="mx-auto mt-16 grid max-w-2xl grid-cols-1 gap-x-8 gap-y-20 lg:mx-0 lg:max-w-none lg:grid-cols-3"> {SortingDate(data).map((post) => ( <button onClick={() => { setSelectedPostId(post.id); // 记录选中的新闻ID setVisible(true); // 打开模态框 }} key={post.id} className="flex flex-col items-start bg-gray-800 rounded-2xl" > <article> <div className="relative w-full"> <img src={post.imageUrl} alt="Thumbnail" className="aspect-[16/9] w-full rounded-2xl bg-gray-300 object-cover sm:aspect-[2/1] lg:aspect-[3/2]" /> <div className="absolute inset-0 rounded-2xl ring-1 ring-inset ring-gray-900/10" /> </div> <div className="max-w-xl"> <div className="mt-8 flex items-center gap-x-4 text-xs mx-2"> <time dateTime={post.datetime} className="text-gray-300"> {post.date} </time> </div> <div className="group relative mx-2"> <h3 className="mt-3 text-lg font-semibold leading-6 text-gray-300 group-hover:text-gray-500"> <a href={post.href}> <span className="absolute inset-0" /> {post.title} </a> </h3> </div> <div className="relative mt-8 flex items-center gap-x-4 mx-2 my-2"> <div className="text-sm leading-6"> <p className="font-semibold text-white"> <a> <span className="absolute inset-0" /> {post.author.name} </a> </p> </div> </div> </div> </article> </button> ))} </div> </div> </div> ); }
同时需要调整NewsModal组件,接收post props来展示对应内容,而不是仅通过id获取。
内容的提问来源于stack exchange,提问作者Lim Jian Hong
相关产品推荐
相关产品推荐

