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

组件挂载时修改Context中URL加载Youtube视频,视频状态异常排查

问题描述

我尝试用React Context复用逻辑,首页通过Youtube API加载全量视频,搜索页加载过滤后的视频。但当前代码中videos状态始终为undefined,无法正常渲染视频组件。相关代码如下:

Context代码

import { createContext, useState, useEffect } from "react";
import { IVideo } from "../@types/video";
import axios from "axios";
import moment from "moment";
import { IContextProps, IVideoContext } from "../@types/context";

export const VideoContext = createContext<IVideoContext>({} as IVideoContext);

export const VideoData = ({ children }: IContextProps) => {
  const [videos, setVideos] = useState<IVideo[]>([]); // videos状态返回undefined
  const [url, setUrl] = useState(``) // url状态在console.log中显示正确值
  const [dependency, setDependency] = useState(``) // 这个状态值也正确
  
 useEffect(() => { 
       load() // 这个函数似乎没生效
   }, [dependency])
 
  async function load() {
    try {
      const response = await axios.get(url) // 手动写url的话这段代码能正常工作
      setVideos(response.data.items)
    }catch(error){
      console.log(error)
    }
  }
  
  function formatViewCount(viewCount: number): string {
    if (viewCount >= 1000000) {
      return `${(viewCount / 1000000).toFixed(0)} mi de visualizações`;
    } else if (viewCount >= 1000) {
      return `${(viewCount / 1000).toFixed(0)} mil visualizações`;
    } else {
      return `${viewCount} visualizações`;
    }
  }

  function getPublishedTime(publishedAt: string) {
    const now = moment();
    const publishedTime = moment(publishedAt);
    const diffDays = now.diff(publishedTime, 'days');

    if (diffDays <= 0) {
      return 'hoje';
    } else if (diffDays === 1) {
      return 'há 1 dia';
    } else if (diffDays <= 7) {
      return `há ${diffDays} dias`;
    } else if (diffDays <= 30) {
      const diffWeeks = Math.floor(diffDays / 7);
      if (diffWeeks === 1) {
        return 'há 1 semana';
      } else {
        return `há ${diffWeeks} semanas`;
      }
    } else if (diffDays <= 365) {
      const diffMonths = Math.floor(diffDays / 30);
      if (diffMonths === 1) {
        return 'há 1 mês';
      } else {
        return `há ${diffMonths} meses`;
      }
    } else {
      const diffYears = Math.floor(diffDays / 365);
      if (diffYears === 1) {
        return 'há 1 ano';
      } else {
        return `há ${diffYears} anos`;
      }
    }
  }
  
    return (
        <VideoContext.Provider value={{ videos, getPublishedTime, formatViewCount, setUrl, setDependency }}>
            {children}
        </VideoContext.Provider>

    )
}

首页代码

import { useContext, useEffect } from "react";
import VideoComponent from "../../components/videoComponent";
import { Container } from "./styles";
import { VideoContext } from "../../context/videoContext";
import { IVideo } from "../../@types/video";
import { useCategoryContext } from "../../context/categoriesContext";

function Home(){

    const {categoryId} = useCategoryContext()
    const { videos, setUrl, setDependency } = useContext(VideoContext)
    const API_KEY = 'AIzaSyBgyeOAGzLblKVCjEI-soVVLkQdNfvApkA'

    useEffect(() => {
        setUrl(`https://youtube.googleapis.com/youtube/v3/videos?part=snippet&part=statistics&chart=mostPopular&maxResults=48&videoCategoryId=${categoryId}&key=${API_KEY}`);
        setDependency(categoryId)
    }, [])

    return (
        <Container>
            {videos.map((video: IVideo) => (
                <VideoComponent video={video} key={video.id} />
            ))}
        </Container>
    )
}

export default Home;

视频组件代码

import { useContext } from "react";
import { IVideos } from "../../@types/video";
import { ChannelImage, Container, ImageBanner, TextCard, TextContainer, Title, TitleContainer } from "./styles";
import { VideoContext } from "../../context/videoContext";

function VideoComponent({video}:IVideos){
   
  const { formatViewCount, getPublishedTime } = useContext(VideoContext)

    return (
      <Container>
        <ImageBanner src={video.snippet.thumbnails.maxres?.url || video.snippet.thumbnails.high?.url} />
        <TitleContainer>
          <ChannelImage>{video.snippet.channelTitle.charAt(0).toUpperCase()}</ChannelImage>
          <TextContainer>
            <Title>{video.snippet.title}</Title>
            <TextCard>{video.snippet.channelTitle}</TextCard>
            <TextCard>{`${formatViewCount(Number(video.statistics.viewCount))} - ${getPublishedTime(video.snippet.publishedAt)}`}</TextCard>
          </TextContainer>
        </TitleContainer>
      </Container>
    )
}

export default VideoComponent;

错误原因排查

  1. 状态更新竞态问题:Home组件的useEffect中同时调用setUrl和setDependency,但React的状态更新是异步批量处理的。当dependency变化触发Context中的useEffect时,url可能还没完成更新,导致load函数使用空字符串请求API,请求失败后setVideos未执行,或返回数据异常导致videos变为undefined。

  2. 冗余的dependency状态:额外维护dependency状态完全没必要,直接监听url的变化就能触发数据加载,多一层状态反而增加了同步风险。

  3. 未处理API返回的边界情况:如果API请求失败或返回的response.data.items为undefined,setVideos会直接把videos设为undefined,而不是保持初始的空数组。

  4. Home组件的useEffect依赖缺失:当前useEffect的依赖数组为空,当categoryId变化时不会重新请求数据,同时首次渲染时也无法保证categoryId已经获取到有效值。


修复方案

1. 修改Context代码,移除冗余状态并优化加载逻辑

import { createContext, useState, useEffect } from "react";
import { IVideo } from "../@types/video";
import axios from "axios";
import moment from "moment";
import { IContextProps, IVideoContext } from "../@types/context";

export const VideoContext = createContext<IVideoContext>({} as IVideoContext);

export const VideoData = ({ children }: IContextProps) => {
  const [videos, setVideos] = useState<IVideo[]>([]);
  const [url, setUrl] = useState(``);
  const [isLoading, setIsLoading] = useState(false); // 添加加载状态

  // 直接监听url变化,移除dependency状态
  useEffect(() => {
    if (!url) return; // 空url不执行请求
    load();
  }, [url])
 
  async function load() {
    setIsLoading(true);
    try {
      const response = await axios.get(url);
      // 处理items可能为undefined的情况,确保始终是数组
      setVideos(response.data.items || []);
    } catch(error){
      console.log(error);
      setVideos([]); // 出错时重置为空数组
    } finally {
      setIsLoading(false);
    }
  }
  
  // 原有的formatViewCount和getPublishedTime函数保持不变
  function formatViewCount(viewCount: number): string {
    if (viewCount >= 1000000) {
      return `${(viewCount / 1000000).toFixed(0)} mi de visualizações`;
    } else if (viewCount >= 1000) {
      return `${(viewCount / 1000).toFixed(0)} mil visualizações`;
    } else {
      return `${viewCount} visualizações`;
    }
  }

  function getPublishedTime(publishedAt: string) {
    const now = moment();
    const publishedTime = moment(publishedAt);
    const diffDays = now.diff(publishedTime, 'days');

    if (diffDays <= 0) {
      return 'hoje';
    } else if (diffDays === 1) {
      return 'há 1 dia';
    } else if (diffDays <= 7) {
      return `há ${diffDays} dias`;
    } else if (diffDays <= 30) {
      const diffWeeks = Math.floor(diffDays / 7);
      if (diffWeeks === 1) {
        return 'há 1 semana';
      } else {
        return `há ${diffWeeks} semanas`;
      }
    } else if (diffDays <= 365) {
      const diffMonths = Math.floor(diffDays / 30);
      if (diffMonths === 1) {
        return 'há 1 mês';
      } else {
        return `há ${diffMonths} meses`;
      }
    } else {
      const diffYears = Math.floor(diffDays / 365);
      if (diffYears === 1) {
        return 'há 1 ano';
      } else {
        return `há ${diffYears} anos`;
      }
    }
  }
  
    return (
        <VideoContext.Provider value={{ videos, isLoading, getPublishedTime, formatViewCount, setUrl }}>
            {children}
        </VideoContext.Provider>
    )
}

2. 修改Home组件,完善依赖并处理加载状态

import { useContext, useEffect } from "react";
import VideoComponent from "../../components/videoComponent";
import { Container } from "./styles";
import { VideoContext } from "../../context/videoContext";
import { IVideo } from "../../@types/video";
import { useCategoryContext } from "../../context/categoriesContext";

function Home(){

    const {categoryId} = useCategoryContext()
    const { videos, isLoading, setUrl } = useContext(VideoContext)
    const API_KEY = 'AIzaSyBgyeOAGzLblKVCjEI-soVVLkQdNfvApkA'

    // 添加categoryId到依赖数组,分类变化时重新请求
    useEffect(() => {
        if (!categoryId) return; // 确保categoryId有效再设置url
        setUrl(`https://youtube.googleapis.com/youtube/v3/videos?part=snippet&part=statistics&chart=mostPopular&maxResults=48&videoCategoryId=${categoryId}&key=${API_KEY}`);
    }, [categoryId])

    if (isLoading) {
        return <div>加载中...</div>; // 加载状态提示
    }

    return (
        <Container>
            {videos.map((video: IVideo) => (
                <VideoComponent video={video} key={video.id} />
            ))}
        </Container>
    )
}

export default Home;

3. 类型定义校验(可选)

确保IVideoContext的类型定义正确,避免videos出现undefined:

// 示例类型定义
export interface IVideoContext {
  videos: IVideo[];
  isLoading: boolean;
  getPublishedTime: (publishedAt: string) => string;
  formatViewCount: (viewCount: number) => string;
  setUrl: (url: string) => void;
}

内容的提问来源于stack exchange,提问作者Leon Murelli SIlveira

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 02:27:01