如何将引用audio标签的useRef值存储到Redux Toolkit中?
解决方案:避免在Redux中存储不可序列化的DOM引用
错误原因
你遇到的A non-serializable value was detected in an action错误,是因为Redux要求action的payload和store中的状态必须是可序列化的值(比如字符串、数字、普通对象、数组这类能转成JSON的类型),而audioRef是React的Ref对象,其current指向的是DOM音频元素,属于浏览器原生对象,无法被序列化,所以不能直接存在Redux里。
可行方案
方案1:用Redux管理音频状态,而非DOM引用
核心思路是:Redux只存储音频的状态数据(比如播放状态、当前音频URL、进度、时长等可序列化的值),而音频DOM元素的操作统一放在它所在的组件(你的App.jsx)中,通过监听Redux状态变化来执行对应的DOM操作。
步骤示例:
- 定义Redux Slice,管理音频相关状态:
// features/audio/audioSlice.js import { createSlice } from '@reduxjs/toolkit'; const initialState = { isPlaying: false, currentAudioUrl: '', currentTime: 0, duration: 0 }; const audioSlice = createSlice({ name: 'audio', initialState, reducers: { setIsPlaying: (state, action) => { state.isPlaying = action.payload; }, setCurrentAudioUrl: (state, action) => { state.currentAudioUrl = action.payload; }, setCurrentTime: (state, action) => { state.currentTime = action.payload; }, setDuration: (state, action) => { state.duration = action.payload; } } }); export const { setIsPlaying, setCurrentAudioUrl, setCurrentTime, setDuration } = audioSlice.actions; export default audioSlice.reducer;
- 在App.jsx中监听Redux状态,控制音频元素:
// App.jsx import { useRef, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { setIsPlaying, setDuration, setCurrentTime } from './features/audio/audioSlice'; function App() { const audioRef = useRef(null); const dispatch = useDispatch(); const { isPlaying, currentAudioUrl } = useSelector(state => state.audio); // 监听播放状态变化,控制音频播放/暂停 useEffect(() => { if (!audioRef.current) return; isPlaying ? audioRef.current.play().catch(err => console.error('播放失败:', err)) : audioRef.current.pause(); }, [isPlaying]); // 监听音频URL变化,加载新音频 useEffect(() => { if (!audioRef.current || !currentAudioUrl) return; audioRef.current.src = currentAudioUrl; audioRef.current.load(); }, [currentAudioUrl]); // 监听音频时长、进度变化,同步到Redux useEffect(() => { const handleLoadedMetadata = () => dispatch(setDuration(audioRef.current.duration)); const handleTimeUpdate = () => dispatch(setCurrentTime(audioRef.current.currentTime)); if (audioRef.current) { audioRef.current.addEventListener('loadedmetadata', handleLoadedMetadata); audioRef.current.addEventListener('timeupdate', handleTimeUpdate); } return () => { if (audioRef.current) { audioRef.current.removeEventListener('loadedmetadata', handleLoadedMetadata); audioRef.current.removeEventListener('timeupdate', handleTimeUpdate); } }; }, [dispatch]); return ( <div> <audio ref={audioRef} /> {/* 其他组件 */} </div> ); } export default App;
- 其他组件通过dispatch控制音频:
比如在播放按钮组件中:
// components/PlayButton.jsx import { useDispatch, useSelector } from 'react-redux'; import { setIsPlaying } from '../features/audio/audioSlice'; function PlayButton() { const dispatch = useDispatch(); const isPlaying = useSelector(state => state.audio.isPlaying); return ( <button onClick={() => dispatch(setIsPlaying(!isPlaying))}> {isPlaying ? '暂停' : '播放'} </button> ); } export default PlayButton;
方案2:用独立模块存储音频引用(适合简单场景)
如果你的应用不需要复杂的状态管理,只是想让多个组件直接访问音频实例,可以创建一个单独的模块来导出这个ref,不需要经过Redux:
- 创建audioInstance.js:
// utils/audioInstance.js import { createRef } from 'react'; export const audioRef = createRef(null);
- 在App.jsx中使用这个ref:
// App.jsx import { audioRef } from './utils/audioInstance'; function App() { return ( <div> <audio ref={audioRef} /> {/* 其他组件 */} </div> ); } export default App;
- 在其他组件中直接导入使用:
// components/VolumeControl.jsx import { audioRef } from '../utils/audioInstance'; function VolumeControl() { const handleVolumeChange = (e) => { audioRef.current && (audioRef.current.volume = e.target.value); }; return ( <input type="range" min="0" max="1" step="0.1" onChange={handleVolumeChange} /> ); } export default VolumeControl;
总结
- Redux的设计初衷是存储可序列化的应用状态数据,不是用来存储DOM引用这类不可序列化的值。
- 优先选择方案1,它更符合Redux的设计理念,也能更好地管理音频相关的状态逻辑;方案2适合小型应用或简单场景。
内容的提问来源于stack exchange,提问作者MissingSemicolon
相关产品推荐
相关产品推荐

