NextJS 13路由二次重渲染崩溃,本地API数据获取异常求助
NextJS 13 路由切换返回后页面报错问题
我在NextJS 13中尝试从本地API获取数据,但切换到其他路由后返回该页面时出现错误,求原因。
错误截图:
相关代码
page.tsx
export function DefaultSideBar() { return ( <SideBar> <Logo></Logo> <SideOption src={AHlogo} title="Auction Browser"></SideOption> </SideBar> ) } export default function Home() { return ( <> <div className={styles.container}> <DefaultSideBar></DefaultSideBar> <MainPage disabled={false}></MainPage> </div> </> ) }
<SideBar /> 组件定义
export function SideBar(props: {children: React.ReactNode}) { return ( <div className={styles.sidebar}> {props.children} </div> ) }
MainPage 组件
export function MainPage(props: {disabled: boolean}) { return ( <div className={styles["page"]}> <h1>Welcome Back!</h1> <hr /> <h2>News</h2> <News></News> </div> ) }
News 组件
export function News() { 'use client'; let [outelems, setOutelems] = useState<JSX.Element[]>([]) let dataref = useRef<NewsPanel[]>() dataref.current = use(getNewsData()); useMemo(() => { console.log("ran") setOutelems((dataref.current ?? []).map( (d, index) => { return ( <a key={index} href={`/news/${index}`} className={styles["news-panel"]}> <div > <h3 className={styles["news-title"]}>{d.title}</h3> <p className={styles["news-desc"]}><span dangerouslySetInnerHTML={{__html: formatDesc(d.description).split(" ").slice(0, 22).join(" ") + (formatDesc(d.description).split(" ").length > 22 ? "..." : "")}}></span> </p> <p className={styles["news-time"]}>{timeConverter(parseInt(d.creation_date.toString()))}</p> </div> </a> ) } ).reverse()) }, []) return ( <div className={styles["news-scroll"]}> {outelems} </div> ) }
问题原因
- 非法使用
use钩子:你在标记了'use client'的客户端组件里调用了React的use钩子,这个钩子是专为服务端组件设计的异步数据获取API,客户端环境下重复渲染(比如路由切换返回)时会触发React的运行时检查,直接报错。 useMemo逻辑缺陷:useMemo的依赖项为空数组,只会执行一次,二次渲染时无法响应数据变化;同时结合错误的use钩子用法,数据获取逻辑完全失效,导致状态更新异常。
修复方案
方案一:客户端组件改用useEffect获取数据
把use钩子替换成useEffect结合异步函数,这是客户端组件数据获取的标准写法:
export function News() { 'use client'; const [outelems, setOutelems] = useState<JSX.Element[]>([]) useEffect(() => { const fetchNews = async () => { const newsData = await getNewsData(); const newsElements = (newsData ?? []).map((d, index) => ( <a key={index} href={`/news/${index}`} className={styles["news-panel"]}> <div> <h3 className={styles["news-title"]}>{d.title}</h3> <p className={styles["news-desc"]}> <span dangerouslySetInnerHTML={{ __html: formatDesc(d.description).split(" ").slice(0,22).join(" ") + (formatDesc(d.description).split(" ").length>22 ? "..." : "") }}></span> </p> <p className={styles["news-time"]}>{timeConverter(parseInt(d.creation_date.toString()))}</p> </div> </a> )).reverse(); setOutelems(newsElements); }; fetchNews(); }, []); return ( <div className={styles["news-scroll"]}> {outelems} </div> ) }
方案二:拆分服务端+客户端组件
如果getNewsData适合在服务端执行(比如不需要浏览器环境),可以把数据获取逻辑移到服务端组件,只把渲染部分留在客户端:
// NewsServer.tsx(服务端组件,无需'use client') export async function NewsServer() { const newsData = await getNewsData(); return <NewsClient data={newsData ?? []} />; } // NewsClient.tsx(客户端组件) 'use client'; export function NewsClient({ data }: { data: NewsPanel[] }) { const outelems = useMemo(() => { return data.map((d, index) => ( <a key={index} href={`/news/${index}`} className={styles["news-panel"]}> <div> <h3 className={styles["news-title"]}>{d.title}</h3> <p className={styles["news-desc"]}> <span dangerouslySetInnerHTML={{ __html: formatDesc(d.description).split(" ").slice(0,22).join(" ") + (formatDesc(d.description).split(" ").length>22 ? "..." : "") }}></span> </p> <p className={styles["news-time"]}>{timeConverter(parseInt(d.creation_date.toString()))}</p> </div> </a> )).reverse(); }, [data]); return ( <div className={styles["news-scroll"]}> {outelems} </div> ) }
之后在MainPage里替换成<NewsServer />即可。
内容的提问来源于stack exchange,提问作者Silkyway
相关产品推荐
相关产品推荐

