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

为何触发‘Invalid hook call’错误?React音频滑块代码问题排查

问题:React组件中Invalid hook call错误排查与修复

尝试基于数组创建audio元素和rc-slider组件控制音频音量,编写了如下React函数组件,但触发了Invalid hook call错误,请求排查原因及修复方案。

原代码

import { useState, useRef, useEffect } from 'react';
import Slider from 'rc-slider';
import 'rc-slider/assets/index.css';

function App() {
  const initialAudioSources = [
    { src: "/fire.mp3", volume: 1 },
    { src: "/ocean.mp3", volume: 1 },
    // Add more audio sources as needed
  ];

  const [audios, setAudios] = useState([]);
  const audioRefs = useRef([]);

  useEffect(() => {
    // Create audio elements and set initial volume for each audio source
    const audioElements = initialAudioSources.map((audioSource) => {
      const audio = new Audio(audioSource.src);
      audio.volume = audioSource.volume;
      audio.loop = true;
      return audio;
    });

    setAudios(audioElements);
    audioRefs.current = audioElements.map(() => useRef(null));

    // Start playing the audios
    audioElements.forEach((audio, index) => {
      if (audioRefs.current[index].current) {
        audioRefs.current[index].current.volume = audio.volume;
        audioRefs.current[index].current.play();
      }
    });
  }, []);

  const handleSliderChange = (newValue, index) => {
    const updatedAudios = audios.map((audio, i) => {
      if (i === index && audioRefs.current[i].current) {
        audioRefs.current[i].current.volume = newValue / 100;
      }
      return audio;
    });

    setAudios(updatedAudios);
  };

  return (
    <>
      <div className="audio-controls">
        {audios.map((audio, index) => (
          <div key={index} className="audio-control">
            <audio ref={audioRefs.current[index]} controls>
              <source src={initialAudioSources[index].src} type="audio/mpeg" />
              Your browser does not support the audio element.
            </audio>
            <div className="slider-wrapper">
              <Slider
                className="slider"
                min={0}
                max={100}
                step={1}
                value={audio.volume * 100}
                onChange={(newValue) => handleSliderChange(newValue, index)}
                vertical
              />
              <p>{audio.volume * 100}%</p>
            </div>
          </div>
        ))}
      </div>
    </>
  );
}

export default App;

错误信息

Error Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:

  1. You might have mismatching versions of React and the renderer (such as React DOM)
  2. You might be breaking the Rules of Hooks
  3. You might have more than one copy of React in the same app

问题原因

核心错误是违反了React Hooks的规则:Hook只能在函数组件的顶层作用域调用,不能在循环、条件语句、嵌套函数(包括useEffect的回调函数)内部调用。

原代码中,audioRefs.current = audioElements.map(() => useRef(null));这一行在useEffect的回调里循环调用useRef,完全不符合Hook的调用规则,直接触发了Invalid hook call错误。

此外,原代码还存在逻辑混淆:同时用了JS创建的Audio实例和DOM中的<audio>元素,两者没有关联,导致音量控制逻辑混乱。

修复方案

修复后的完整代码

import { useState, useRef, useEffect } from 'react';
import Slider from 'rc-slider';
import 'rc-slider/assets/index.css';

function App() {
  const initialAudioSources = [
    { src: "/fire.mp3", volume: 1 },
    { src: "/ocean.mp3", volume: 1 },
    // Add more audio sources as needed
  ];

  // 提前在组件顶层初始化ref数组,对应每个DOM audio元素
  const audioRefs = useRef(initialAudioSources.map(() => useRef(null)));
  // 用state存储每个音频的音量百分比,用于滑块显示和更新
  const [volumes, setVolumes] = useState(initialAudioSources.map(item => item.volume * 100));

  useEffect(() => {
    // 初始化每个音频的音量设置
    audioRefs.current.forEach((ref, index) => {
      if (ref.current) {
        ref.current.volume = initialAudioSources[index].volume;
        ref.current.loop = true;
        // 注意:浏览器自动播放策略要求必须由用户交互触发播放,建议添加按钮控制
        // ref.current.play().catch(err => console.error('播放失败:需要用户交互', err));
      }
    });
  }, []);

  const handleSliderChange = (newValue, index) => {
    // 更新音量状态,同步滑块显示
    setVolumes(prevVolumes => {
      const updated = [...prevVolumes];
      updated[index] = newValue;
      return updated;
    });
    // 直接通过ref控制DOM audio元素的音量
    if (audioRefs.current[index].current) {
      audioRefs.current[index].current.volume = newValue / 100;
    }
  };

  return (
    <>
      <div className="audio-controls">
        {initialAudioSources.map((source, index) => (
          <div key={index} className="audio-control">
            <audio ref={audioRefs.current[index]} controls>
              <source src={source.src} type="audio/mpeg" />
              Your browser does not support the audio element.
            </audio>
            <div className="slider-wrapper">
              <Slider
                className="slider"
                min={0}
                max={100}
                step={1}
                value={volumes[index]}
                onChange={(newValue) => handleSliderChange(newValue, index)}
                vertical
              />
              <p>{volumes[index]}%</p>
            </div>
          </div>
        ))}
      </div>
    </>
  );
}

export default App;

关键修复点

  1. 修正Hook调用位置:所有useRef都在组件顶层调用,符合React Hooks的规则,彻底解决Invalid hook call错误。
  2. 简化逻辑,分离状态与DOM控制:
    • 用volumes state存储每个音频的音量百分比,专门用于滑块的显示和更新。
    • 直接通过audioRefs操作DOM中的<audio>元素,不需要额外创建JS的Audio实例,避免逻辑混淆。
  3. 适配浏览器自动播放策略:注释了自动播放代码,提示需要通过用户交互(比如点击按钮)触发播放,避免浏览器报错。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 09:35:55