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

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

问题分析与修复方案

核心问题

  1. 骨架屏渲染逻辑错误:原代码在allVideos.map内部判断加载状态,但allVideos初始为空数组,加载时根本不会进入循环,骨架屏无法渲染
  2. 状态判断逻辑无效:isVideoLoading = isLoading && !video.id,加载阶段没有video数据,该条件永远不成立
  3. 视频详情复用问题:原代码只获取第一个视频的详情,导致所有视频共用同一组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;

修复说明

  1. 分离骨架屏与真实内容渲染:加载状态时直接渲染20个骨架屏,无需依赖视频列表数据
  2. 修复视频详情逻辑:批量获取所有视频的详情并存储,每个视频使用自己的views和duration
  3. 关键词切换时清空旧数据:避免新旧数据混合,保证加载状态的纯净性
  4. 补全Skeleton组件导入:确保骨架屏库能正常工作

内容的提问来源于stack exchange,提问作者christian

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 02:12:09