React项目中react-loading-skeleton骨架屏无法显示求助
问题:数据加载时无法显示最多20个骨架屏
我通过YouTube Data API获取数据,映射后展示在页面上;同时设置了存储不同词汇的keyword变量,用户点击任意词汇会重新获取数据并更新页面。希望在数据获取过程中(isLoading状态从false变为true时),页面显示最多20个骨架屏,但目前该效果无法正常实现。
代码片段
VideoDetail.jsx
import styled from "styled-components"; import SearchFeed from './SearchFeed'; import GroupsBar from './GroupsBar'; import { fetchVideos } from '../redux/extraReducer'; import { useDispatch, useSelector } from 'react-redux'; import { useEffect, useState } from "react"; import moment from "moment"; import numeral from "numeral"; import request from "../redux/request"; import InfiniteScroll from "react-infinite-scroll-component"; import "./styling/styling.css" import { fetchNextPage } from "../redux/videoReducer"; import SkeletonVideo from "./skeleton/SkeletonVideo"; const VideoDetail = ({ groupHandler }) => { const [views, setViews] = useState(""); const [timing, setTiming] = useState(""); const [channelIcons, setChannelIcons] = useState({}); const [allVideos, setAllVideos] = useState([]); const isLoading = useSelector((state)=> state.user.youtube?.isLoading) console.log(`this is ${isLoading}`) const videos = useSelector((state) => state.user.youtube.obj); const dispatch = useDispatch(); useEffect(() => { dispatch(fetchVideos()) .then((response) => { // console.log(response) }) .catch((error) => console.log(error)); }, [dispatch]); const fetchData = (()=>{ dispatch(fetchVideos()) }) useEffect(() => { if (videos && videos.items && videos.items.length > 0) { setAllVideos((prevVideos) => [...prevVideos, ...videos.items]); } }, [videos]); useEffect(() => { if (videos && videos.items && videos.items.length > 0) { const videoIds = videos.items.map((video) => video.id); const getVideoDetails = async () => { try { const { data } = await request("/videos", { params: { part: "contentDetails,statistics", id: videoIds.join(","), }, }); console.log("API Response:", data); if (data.items && data.items.length > 0) { const { contentDetails, statistics } = data.items[0]; console.log("Content Details:", contentDetails); console.log("Statistics:", statistics); setTiming(contentDetails?.duration || ""); setViews(statistics?.viewCount || ""); } else { console.log("No items found in the API response."); } } catch (error) { console.error("Error fetching video details:", error); } }; getVideoDetails(); } }, [videos]); // 代码片段中此处存在语法错误,已省略无效部分 return ( <> <Container> <h2>Recommended</h2> <GroupsBar groupHandler={groupHandler} /> <VideoBox> { allVideos .map((video) => { const { id, snippet: { channelId, channelTitle, title, publishedAt, thumbnails: { medium }, }, } = video; const isVideoLoading = isLoading && !video.id; const seconds = moment.duration(timing).asSeconds(); const duration = moment.utc(seconds * 1000).format("mm:ss"); return ( <div> <VideoContain> { isVideoLoading ?( <span> { [...Array(20).fill()].map((_, index)=>{ return( <SkeletonVideo/> ) })} </span> ) : ( <> <SearchFeed video={video} id={id} channelId={channelId} /> <VideoData> {medium && <img src={medium.url} alt="" />} <p >{duration}</p> </VideoData> <AvatarPhoto> <img src={channelIcons[channelId]?.url} alt="" /> <h5>{title}</h5> </AvatarPhoto> <DataIcon> <p>{channelTitle}</p> </DataIcon> <TitleData> <span>{numeral(views).format("0.a")} views </span> <span>{moment(publishedAt).fromNow()}</span> </TitleData> </> )} </VideoContain> </div> ); })} </VideoBox> </Container> </> ); }; export default VideoDetail;
SkeletonVideo.js
import { Skeleton, SkeletonTheme } from "react-loading-skeleton"; // 补全缺失导入 const SkeletonVideo = () => { return ( <div style={{ width: '100%', margin: '1rem 0', position:"relative" }}> <SkeletonTheme color="#232b2340" highlightColor="#26282b"> <Skeleton height={180} /> <div style={{display: 'flex', alignItems: 'center', gap: '0.5rem', marginTop: '0.5rem'}}> <Skeleton circle height={40} width={40} /> <div> <Skeleton height={20} width="75%" /> <Skeleton height={16} width="50%" /> </div> </div> </SkeletonTheme> </div> ); }; export default SkeletonVideo
问题分析与修复方案
核心问题
- 骨架屏渲染逻辑错误:原代码在
allVideos.map内部判断加载状态,但allVideos初始为空数组,加载时根本不会进入循环,骨架屏无法渲染 - 状态判断逻辑无效:
isVideoLoading = isLoading && !video.id,加载阶段没有video数据,该条件永远不成立 - 视频详情复用问题:原代码只获取第一个视频的详情,导致所有视频共用同一组views和duration数据
修复代码
import styled from "styled-components"; import SearchFeed from './SearchFeed'; import GroupsBar from './GroupsBar'; import { fetchVideos } from '../redux/extraReducer'; import { useDispatch, useSelector } from 'react-redux'; import { useEffect, useState } from "react"; import moment from "moment"; import numeral from "numeral"; import request from "../redux/request"; import "./styling/styling.css" import SkeletonVideo from "./skeleton/SkeletonVideo"; const VideoDetail = ({ groupHandler }) => { const [videoDetails, setVideoDetails] = useState({}); const [channelIcons, setChannelIcons] = useState({}); const [allVideos, setAllVideos] = useState([]); const isLoading = useSelector((state)=> state.user.youtube?.isLoading) const videos = useSelector((state) => state.user.youtube.obj); const dispatch = useDispatch(); // 关键词切换时重新加载数据并清空旧列表 useEffect(() => { setAllVideos([]); dispatch(fetchVideos()) .catch((error) => console.log(error)); }, [dispatch, groupHandler]); // 合并新视频数据并获取详情 useEffect(() => { if (videos && videos.items && videos.items.length > 0) { setAllVideos(prev => [...prev, ...videos.items]); fetchVideoDetails(videos.items); fetchChannelIcons(videos.items); } }, [videos]); // 批量获取视频详情 const fetchVideoDetails = async (videoItems) => { const videoIds = videoItems.map(video => video.id); try { const { data } = await request("/videos", { params: { part: "contentDetails,statistics", id: videoIds.join(","), }, }); if (data.items && data.items.length > 0) { const details = {}; data.items.forEach(item => { details[item.id] = { duration: moment.utc(moment.duration(item.contentDetails.duration).asSeconds() * 1000).format("mm:ss"), views: numeral(item.statistics.viewCount).format("0.a") }; }); setVideoDetails(prev => ({ ...prev, ...details })); } } catch (error) { console.error("Error fetching video details:", error); } }; // 批量获取频道图标 const fetchChannelIcons = async (videoItems) => { const channelIds = [...new Set(videoItems.map(video => video.snippet.channelId))]; try { const { data } = await request("/channels", { params: { part: "snippet", id: channelIds.join(","), }, }); if (data.items && data.items.length > 0) { const icons = {}; data.items.forEach(item => { icons[item.id] = item.snippet.thumbnails.default; }); setChannelIcons(prev => ({ ...prev, ...icons })); } } catch (error) { console.error("Error fetching channel icons:", error); } }; return ( <Container> <h2>Recommended</h2> <GroupsBar groupHandler={groupHandler} /> <VideoBox> {/* 加载状态直接渲染20个骨架屏 */} {isLoading ? ( [...Array(20).fill()].map((_, index) => ( <SkeletonVideo key={index} /> )) ) : ( allVideos.map((video) => { const { id, snippet: { channelId, channelTitle, title, publishedAt, thumbnails: { medium }, }, } = video; const details = videoDetails[id] || {}; return ( <VideoContain key={id}> <SearchFeed video={video} id={id} channelId={channelId} /> <VideoData> {medium && <img src={medium.url} alt={title} />} <p>{details.duration || "--:--"}</p> </VideoData> <AvatarPhoto> <img src={channelIcons[channelId]?.url || ""} alt={channelTitle} /> <h5>{title}</h5> </AvatarPhoto> <DataIcon> <p>{channelTitle}</p> </DataIcon> <TitleData> <span>{details.views || "0"} views </span> <span>{moment(publishedAt).fromNow()}</span> </TitleData> </VideoContain> ); }) )} </VideoBox> </Container> ); }; export default VideoDetail;
修复说明
- 分离骨架屏与真实内容渲染:加载状态时直接渲染20个骨架屏,无需依赖视频列表数据
- 修复视频详情逻辑:批量获取所有视频的详情并存储,每个视频使用自己的views和duration
- 关键词切换时清空旧数据:避免新旧数据混合,保证加载状态的纯净性
- 补全Skeleton组件导入:确保骨架屏库能正常工作
内容的提问来源于stack exchange,提问作者christian
相关产品推荐
相关产品推荐

