React v18直接访问或硬刷新文章详情页出现空白页报错
React v18 单篇博客硬刷新白屏问题修复
问题现象
首个React v18.1开发的博客项目存在以下异常:
- 从博客列表页点击跳转至单篇文章详情页时,页面可正常加载
- 通过浏览器刷新按钮硬刷新详情页、或直接在地址栏输入详情页URL访问时,页面显示空白
- 现象表现为博客数据必须经过博客列表页的加载流程才能正常获取
问题根因
控制台报错核心为Uncaught TypeError: Cannot read properties of undefined (reading 'Image_URL'),触发原因:
- App组件中博客数据为异步请求获取,初始状态
blogData为空数组 - 硬刷新详情页时,Singlepost组件会在数据请求完成前提前执行首次渲染
- 此时对空数组执行
find方法查询对应ID的文章会返回undefined,直接读取undefined的Image_URL等属性就会导致组件崩溃,页面白屏 - 从列表页跳转时,用户在列表页停留期间数据已经加载完成,因此不会触发该报错
原有代码参考
index.js 路由配置
import React from 'react'; import ReactDOM from 'react-dom/client'; import { BrowserRouter, Routes, Route } from "react-router-dom"; import './index.css'; import './global.css'; import App from './App'; import About from './components/About'; import Contact from './components/Contact'; import NotFound from './components/NotFound'; import Blog from './components/Blog'; import Doctors from './components/Doctors'; import registerServiceWorker from './registerServiceWorker'; import Singlepost from './components/singlepost'; import Home from './components/Home'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <BrowserRouter> <Routes> <Route path="/" element={<Home />} /> <Route path="/" element={<App />} > <Route path="Doctors" element={<Doctors />} /> <Route path="Blog" element={<Blog />} /> <Route path="blog/:articleId" element={<Singlepost />} /> <Route path="About" element={<About />} /> <Route path="Contact" element={<Contact />} /> <Route path="*" element={<NotFound />} /> </Route> </Routes> </BrowserRouter> ); registerServiceWorker();
App.js 根组件代码
import { useState, useEffect } from "react"; import Header from "./components/header/header"; import Footer from "./components/footer"; import { Outlet } from "react-router-dom"; function App() { const [blogData, setBlogData] = useState([]); const [url, setUrl] = useState(`${process.env.PUBLIC_URL}/blogpost.json`); const dataFetch = async () => { const response = await fetch(url, { headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' } }); const data = await response.json(); setBlogData(data); } useEffect(() => { dataFetch() }, []); return ( <> <Header /> <div className="flex flex-col my-20"> <Outlet context={[blogData]} /> </div> <Footer /> </> ); } export default App;
Blog.js 列表页组件代码
import React, { useState, useMemo, useEffect } from 'react' import { Link, Outlet, useOutletContext, } from 'react-router-dom'; import Pagination from './Pagination'; let PageSize = 10; const MAX_LENGTH = 500; export default function Blog() { const [articles] = useOutletContext(); const [currentPage, setCurrentPage] = useState(1); let currentTableData = useMemo(() => { const firstPageIndex = (currentPage - 1) * PageSize; const lastPageIndex = firstPageIndex + PageSize; return articles.slice(firstPageIndex, lastPageIndex); }, [currentPage, articles]); function slugify(text) { return text.toString().toLowerCase() .replace(/\s+/g, '-') .replace(/[^\w\-]+/g, '') .replace(/\-\-+/g, '-') .replace(/^-+/, '') .replace(/-+$/, ''); } return ( <> { currentTableData.map(doc => ( <div key={crypto.randomUUID()} className="flex flex-col md:flex-row w-full m-auto mb-4 shadow rounded border"> <div> <img src={doc.Image_URL} alt="doctor" className="w-full h-64 md:h-auto md:w-64 rounded-l md:mr-4 max-h-[200px]" /> </div> <div className='flex flex-col'> <Link to={`/blog/${doc.ID}`} className='font-semibold'>{doc.Title}</Link> <div className='flex flex-col'> {doc.Content.length > MAX_LENGTH ? ( <div className='prose lg:prose-xl' dangerouslySetInnerHTML={{ __html: doc.Content.substring(0, MAX_LENGTH) }}></div> ) : <p className='prose lg:prose-xl' dangerouslySetInnerHTML={{ __html: doc.Content }} ></p> } <span>{doc.Category}</span> </div> </div> </div> )) } <Outlet context={[articles]} /> <Pagination key={crypto.randomUUID()} data-link={crypto.randomUUID()} className="pagination-bar" currentPage={currentPage} totalCount={articles.length} pageSize={PageSize} onPageChange={page => setCurrentPage(page)} /> </> ) }
Singlepost.js 单篇文章组件原有代码
import React from 'react' import { useOutletContext, useParams } from 'react-router-dom'; export default function Singlepost() { let params = useParams(); const [blogData] = useOutletContext(); const blogPost = blogData.find(blog => blog.ID === parseInt(params.articleId)); return ( <div className="flex flex-col md:flex-row w-full m-auto mb-4 shadow rounded border"> <div> <img src={blogPost.Image_URL} alt="doctor" className="w-full h-64 md:h-auto md:w-64 rounded-l md:mr-4 max-h-[200px]" /> </div> <div className='flex flex-col'> <h2 className='font-semibold'>{blogPost.Title}</h2> <div className='flex flex-col'> <div className='prose lg:prose-xl' dangerouslySetInnerHTML={{ __html: blogPost.Content }}></div> <span>{blogPost.Category}</span> </div> </div> </div> ) }
控制台报错信息
singlepost.js:17 Uncaught TypeError: Cannot read properties of undefined (reading 'Image_URL') at Singlepost (singlepost.js:17:1) at renderWithHooks (react-dom.development.js:16175:1) at mountIndeterminateComponent (react-dom.development.js:20913:1) at beginWork (react-dom.development.js:22416:1) at HTMLUnknownElement.callCallback (react-dom.development.js:4161:1) at Object.invokeGuardedCallbackDev (react-dom.development.js:4210:1) at invokeGuardedCallback (react-dom.development.js:4274:1) at beginWork$1 (react-dom.development.js:27405:1) at performUnitOfWork (react-dom.development.js:26513:1) at workLoopSync (react-dom.development.js:26422:1) react-dom.development.js:18572 The above error occurred in the <Singlepost> component: at Singlepost (http://localhost:3000/static/js/bundle.js:1857:75) at Outlet (http://localhost:3000/static/js/bundle.js:68128:26) at div at App (http://localhost:3000/static/js/bundle.js:38:82) at Routes (http://localhost:3000/static/js/bundle.js:68220:5) at Router (http://localhost:3000/static/js/bundle.js:68153:15) at BrowserRouter (http://localhost:3000/static/js/bundle.js:66962:5)
修复方案
1. 给Singlepost组件增加空值判断和加载状态
这是最直接的修复方式,在数据未返回、或者找不到对应文章时,不要直接渲染文章内容,先展示加载状态或者提示,修改后的Singlepost代码如下:
import React from 'react' import { useOutletContext, useParams } from 'react-router-dom'; export default function Singlepost() { let params = useParams(); const [blogData] = useOutletContext(); // 数据还没加载完成时显示加载提示 if (blogData.length === 0) { return <div className="w-full m-auto text-center py-10">文章加载中...</div> } const blogPost = blogData.find(blog => blog.ID === parseInt(params.articleId)); // 找不到对应文章时显示提示 if (!blogPost) { return <div className="w-full m-auto text-center py-10">文章不存在</div> } return ( <div className="flex flex-col md:flex-row w-full m-auto mb-4 shadow rounded border"> <div> <img src={blogPost.Image_URL} alt="文章封面" className="w-full h-64 md:h-auto md:w-64 rounded-l md:mr-4 max-h-[200px]" /> </div> <div className='flex flex-col p-4'> <h2 className='font-semibold text-xl mb-2'>{blogPost.Title}</h2> <div className='flex flex-col'> <div className='prose lg:prose-xl' dangerouslySetInnerHTML={{ __html: blogPost.Content }}></div> <span className="text-gray-500 mt-2">分类:{blogPost.Category}</span> </div> </div> </div> ) }
2. (可选优化)全局增加加载状态
可以在App组件中增加加载标识,数据未加载完成时统一显示全局加载提示,避免每个子组件单独判断:
// App.js修改部分 function App() { const [blogData, setBlogData] = useState([]); const [loading, setLoading] = useState(true); // 新增加载状态 const [url, setUrl] = useState(`${process.env.PUBLIC_URL}/blogpost.json`); const dataFetch = async () => { try { setLoading(true); const response = await fetch(url, { headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' } }); const data = await response.json(); setBlogData(data); } catch (err) { console.error('博客数据加载失败', err); } finally { setLoading(false); // 请求完成不管成功失败都关闭加载状态 } } useEffect(() => { dataFetch() }, []); // 加载中显示全局提示 if (loading) { return ( <> <Header /> <div className="flex flex-col my-20 items-center"> <div>数据加载中,请稍候...</div> </div> <Footer /> </> ) } return ( <> <Header /> <div className="flex flex-col my-20"> <Outlet context={[blogData]} /> </div> <Footer /> </> ); }
3. 冗余代码与性能问题清理
- Blog组件中写的
<Outlet context={[articles]} />属于无效代码,因为/blog/:articleId是App的直接子路由,不是Blog的子路由,Singlepost组件不会渲染在Blog的Outlet位置,这行可以直接删除,避免后续维护产生误解。 - 列表渲染时不要用
crypto.randomUUID()作为key,随机key会导致列表每次重渲染都重新创建DOM,浪费性能,直接用文章的doc.ID作为key即可。
内容的提问来源于stack exchange,提问作者spec10
相关产品推荐
相关产品推荐

