You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React v18直接访问或硬刷新文章详情页出现空白页报错

React v18 单篇博客硬刷新白屏问题修复

问题现象

首个React v18.1开发的博客项目存在以下异常:

  • 从博客列表页点击跳转至单篇文章详情页时,页面可正常加载
  • 通过浏览器刷新按钮硬刷新详情页、或直接在地址栏输入详情页URL访问时,页面显示空白
  • 现象表现为博客数据必须经过博客列表页的加载流程才能正常获取

问题根因

控制台报错核心为Uncaught TypeError: Cannot read properties of undefined (reading 'Image_URL'),触发原因:

  1. App组件中博客数据为异步请求获取,初始状态blogData为空数组
  2. 硬刷新详情页时,Singlepost组件会在数据请求完成前提前执行首次渲染
  3. 此时对空数组执行find方法查询对应ID的文章会返回undefined,直接读取undefined的Image_URL等属性就会导致组件崩溃,页面白屏
  4. 从列表页跳转时,用户在列表页停留期间数据已经加载完成,因此不会触发该报错

原有代码参考

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.02 23:03:31