React首页从其他路由返回时重复渲染(重复调用API)问题排查
从其他路由返回首页时组件重复渲染并重复调用API的原因
我遇到一个问题:从其他路由返回首页时,React组件会重新渲染并重复调用API。按我的理解,用Link标签跳转页面时React应该保留之前的页面状态,请问是什么原因导致的重复渲染?
首页组件
const Homepage = () => { const [header, setHeader] = useState([]); const [imageindex, setImageindex] = useState(0); const [error, setError] = useState(""); const increaseIndex = () => { setImageindex((imageindex + 1) % header.length); }; useEffect(() => { const fetchData = async () => { try { const response = await axios.get(`${baseurl}`); setHeader(response.data); } catch (error) { setError("Error fetching data:", error); } }; fetchData(); }, []); useEffect(() => { const imageTransition = setTimeout(increaseIndex, 4000); return () => clearTimeout(imageTransition); }, [imageindex, header]); return ( <div> <Helmet> <title>The Third Front</title> <meta name="description" content="Homepage of a satirical news blog" /> </Helmet> <AbsoluteNavbar className="absnav"> <Navbar /> </AbsoluteNavbar> {error && <p style={{ color: "red" }}>{error}</p>} <div className="header-wrapper"> {header && header.map((index) => ( <Link to={`/news/${index.slug}`} className="header" key={index.img.src} style={{ translate: `${-100 * imageindex}%` }} > <div className="background-layer" style={{ backgroundImage: index ? `linear-gradient(to bottom, rgba(0, 0, 0, 0) 20%, rgba(0, 0, 0, 1)), url(${index.img.src})` : "", backgroundPosition: index.img.position ? `${index.img.position}` : "center", }} ></div> <div className="text-layer"> <Hero heading={index.heading} author={index.author} /> </div> </Link> ))} </div> <MainContent /> <Footer /> </div> ); }; export default Homepage;
MainContent组件
/* eslint-disable react-hooks/exhaustive-deps */ import React, { useState, useEffect } from "react"; import axios from "axios"; import "./main-content.css"; import { Link } from "react-router-dom"; const baseurl = import.meta.env.VITE_BASE_URL; const MainContent = () => { const [data, setData] = useState([]); const [sidebardata, setSidebardata] = useState([]) const [mobileView, setMobileView] = useState(); const [page, setPage] = useState(1); const [loading, setLoading] = useState(true); const [cat, setCat] = useState(); const [catdata, setCatdata] = useState([]) const [error, setError] = useState(''); const fetchInfo = async() => { setLoading(true); const link = cat? `${baseurl}/?page=${page}&cat=${cat}`: `${baseurl}/?page=${page}` axios .get(link) .then((response) => { setData((prevNews) => [...prevNews, ...response.data]); setLoading(false); }) .catch((error) => { setError("Error fetching data:", error); }); }; const handleLoadMore = () => { setPage(page + 1); }; const handleResize = () => { if (window.innerWidth < 1080) { setMobileView(true); } else { setMobileView(false); } }; const handleCatchange = (e) =>{ setCat(e.target.value) setPage(1); setData([]); } useEffect(() => { fetchInfo(); }, [page, cat]); useEffect(() => { const sidebarlink = `${baseurl}/sidebar`; axios .get(sidebarlink) .then((response) => { setSidebardata((prevNews) => [...prevNews, ...response.data]); }) .catch((error) => { setError("Error fetching sidebar data:", error); }); axios .get(`${baseurl}/categories`) .then((response) => { setCatdata(response.data) }) handleResize(); window.addEventListener("resize", handleResize); return () => { window.removeEventListener("resize", handleResize); }; }, []); return ( <div className="main"> {error && <p>{error}</p>} <div className="content-wrap"> <div className="select"> <h2>Sort News By categories</h2> <select onChange={handleCatchange}> <option value="">All</option> {catdata.map((e, index) => ( <option key={index} value={e.title}>{e.title}</option> ))} </select> </div> <div className="content"> {data.map((e, index) => { const itemWrapClass = index % 2 === 0 ? "itemwrap1" : "itemwrap2"; return ( <Link to={`/news/${e.slug}`} className="item" key={index}> <div className={itemWrapClass}> <div className="gradient"> <img id="img" src={e.img.src} alt="News Image" /> </div> <div className="heading-p"> <hr /> <h2>{e.heading}</h2> <p> {e.subheading} </p> <div className="catog"> Categories: {e.category ? ( e.category.map((cat) => ( <span className='px-1 text-sky' key={cat._id}>{cat.title}</span> )) ) : ( <span>No categories available</span> )} </div> </div> </div> </Link> ); })} <div className="loading-wrap"> {loading ? ( <div className="lds-ring"> <div></div> <div></div> <div></div> <div></div> </div> ) : ( <button id="loadmore" onClick={handleLoadMore}> Load More </button> )} </div> </div> </div> </div> ); }; export default MainContent;
新闻详情页组件
import React from "react"; import Navbar from "../navbar/navbar"; import Footer from "../footer"; import "./newspage.css"; import { useState, useEffect } from "react"; import axios from "axios"; import { useParams } from "react-router-dom"; import { Helmet } from "react-helmet"; const baseurl = import.meta.env.VITE_BASE_URL; const Newspage = () => { const [data, setData] = useState([]); const { slug } = useParams(); useEffect(() => { axios .get(`${baseurl}/news/${slug}`) .then((response) => { setData(response.data[0]); }) .catch((error) => { console.error("Error fetching data:", error); setData(null); }); }); const renderHtmlContent = (html) => { return { __html: html }; }; if (!data) { throw new Error("Data not found"); } return ( <> <Helmet> <title>{data.heading}</title> <meta name="description" content="News page" /> </Helmet> <div className="header" style={{ backgroundImage: data.img ? `url(${data.img.src})` : "", backgroundPosition: data.img ? `${data.img.position}` : "center", }} > <Navbar /> </div> <div className="newsbody"> <div className="news-hero-text"> <h1>{data.heading}</h1> <p className="subheading">{data.subheading}</p> <p> <em>{data.author}</em> </p> </div> </div> </> ); }; export default Newspage;
React路由配置
import { RouterProvider, createBrowserRouter} from "react-router-dom"; import Homepage from './homepage/homepage'; import Newspage from './newspage/newspage'; const Router = () =>{ const router = createBrowserRouter([ { path: "/", element: <Homepage/> }, { path: "/news/:slug", element: <Newspage/>, errorElement: <NotFound/> }, ]); return <RouterProvider router={router} />; } export default Router;
问题原因分析
组件卸载与重新挂载:React Router的默认逻辑是,路由切换时离开的路由组件会被完全卸载,进入的路由组件重新挂载。所以从详情页返回首页时,
Homepage及其子组件MainContent都会被重新挂载,触发组件初始化流程。useEffect执行逻辑:你在Homepage和MainContent中使用了空依赖数组的useEffect,这类钩子会在组件挂载时执行一次。当组件重新挂载,这些useEffect会再次运行,导致API被重复调用。无状态持久化机制:首页的状态(如新闻列表、轮播数据)仅存储在组件内部,组件卸载后状态直接丢失,重新挂载时只能重新请求数据。
解决思路
- 组件缓存:使用第三方库实现类似
keep-alive的功能,让首页组件在路由切换时不被卸载,保留内部状态。 - 全局状态管理:将首页需要的数据存入Context或Redux这类全局状态容器,首次请求后复用全局数据,避免重复请求。
- 本地存储缓存:请求成功后将数据存入
localStorage或sessionStorage,组件挂载时优先读取本地数据,不存在再发起请求,还可设置缓存过期时间。 - 路由级别预加载:利用React Router v6的
loader函数在路由层面预加载数据,数据会被路由缓存,返回首页时直接复用。
内容的提问来源于stack exchange,提问作者shedfur
相关产品推荐
相关产品推荐

