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

React Context引发列表全重渲染及状态持久化问题求助

问题根源

你现在的核心问题是:全局状态(Context/Redux)里用了单一的布尔值isPlaying,所有MainSound组件都共享这个状态,所以点击任意列表项时,所有项都会同步切换播放状态。而本地useState是每个组件独立维护状态,所以没问题,但路由切换后状态丢失——因此需要把全局状态从「是否播放」改成「当前正在播放的音频唯一标识」,同时做持久化。


解决方案步骤

1. 修改PlayingContext的状态结构

把原来单一的isPlaying布尔值,改成记录当前播放音频的唯一ID,同时加入localStorage持久化:

// playing-context.jsx
import { createContext, useState, useEffect, useRef } from 'react';

const PlayingContext = createContext({
  currentPlayingId: null,
  togglePlay: () => {}
});

export const PlayingProvider = ({ children }) => {
  // 从localStorage读取持久化的播放状态
  const [currentPlayingId, setCurrentPlayingId] = useState(() => {
    return localStorage.getItem('currentPlayingId') || null;
  });
  // 全局音频实例,确保同一时间只有一个音频播放
  const audioRef = useRef(null);

  // 播放/切换音频的逻辑
  const togglePlay = (id, audioSource) => {
    if (currentPlayingId === id) {
      // 暂停当前音频
      audioRef.current?.pause();
      setCurrentPlayingId(null);
    } else {
      // 停止之前的音频,播放新音频
      if (audioRef.current) audioRef.current.pause();
      audioRef.current = new Audio(audioSource);
      audioRef.current.play();
      setCurrentPlayingId(id);
    }
  };

  // 状态更新时同步到localStorage
  useEffect(() => {
    localStorage.setItem('currentPlayingId', currentPlayingId);
  }, [currentPlayingId]);

  return (
    <PlayingContext.Provider value={{ currentPlayingId, togglePlay }}>
      {children}
    </PlayingContext.Provider>
  );
};

export default PlayingContext;

2. 修正MainSoundList的代码

  • 获取Firestore数据时保留文档唯一ID(每个音频的标识)
  • 列表项key改用音频ID(不要用Math.random(),避免React渲染错误)
  • 点击事件调用Context的togglePlay方法:
// MainSoundList.jsx
import React, { useState, useEffect, useContext } from 'react';
import MainSound from './MainSound';
import MainSoundSkeleton from './MainSoundSkeleton';
import classes from './MainSoundList.module.scss';
import { db } from '../../../../config/firebase-config';
import { getDocs, collection } from 'firebase/firestore';
import PlayingContext from '../../../../context/playing-context';

const MainSoundList = () => {
  const [sounds, setSounds] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const { togglePlay } = useContext(PlayingContext);

  useEffect(() => {
    const getSounds = async () => {
      try {
        setIsLoading(true);
        const data = await getDocs(collection(db, 'sounds'));
        // 保留每个文档的ID作为音频唯一标识
        const sounds = data.docs.map((doc) => ({
          id: doc.id,
          ...doc.data()
        }));
        setSounds(sounds);
      } catch (error) {
        console.log(error);
      } finally {
        setIsLoading(false);
      }
    };
    getSounds();
  }, []);

  const soundsList = sounds.map((sound) => (
    <MainSound
      key={sound.id}
      id={sound.id}
      title={sound.title}
      imageSource={sound.imageSource}
      audioSource={sound.audioSource}
      onClick={() => togglePlay(sound.id, sound.audioSource)}
    />
  ));

  return (
    <>
      {isLoading ? (
        <MainSoundSkeleton />
      ) : (
        <ul className={classes['main__sounds']}>{soundsList}</ul>
      )}
    </>
  );
};

export default MainSoundList;

3. 修改MainSound组件

不再依赖全局布尔值,通过自身ID和currentPlayingId对比判断播放状态:

// MainSound.jsx
import React, { useContext } from 'react';
import classes from './MainSound.module.scss';
import svg from '../../../../assets/svg/sprite.svg';
import PlayingContext from '../../../../context/playing-context';

const MainSound = ({ id, imageSource, title, audioSource, onClick }) => {
  const { currentPlayingId } = useContext(PlayingContext);
  // 自身ID等于全局记录的ID,说明正在播放
  const isPlaying = currentPlayingId === id;

  const playClickHandler = () => {
    onClick();
  };

  return (
    <li className={classes['main__sound']}>
      <div
        className={`
        ${classes.box} ${isPlaying ? classes.isPlaying : ''}
      `}>
        <img className={classes['box__img']} src={imageSource} alt={title} />

        <button onClick={playClickHandler} className={classes.box__playicon}>
          <svg>
            <use xlinkHref={`${svg}#icon-${isPlaying ? 'pause' : 'play'}`}></use>
          </svg>
        </button>

        <button className={classes.box__hearticon}>
          <svg>
            <use xlinkHref={`${svg}#icon-heart`}></use>
          </svg>
        </button>

        <div className={`${classes.box__menu} ${isPlaying ? '' : classes.hidden}`}>
          <div className={classes.box__icons}>
            <svg className={classes['icon__volume']}>
              <use xlinkHref={`${svg}#icon-volume-2`}></use>
            </svg>
          </div>
        </div>
      </div>

      <div className={classes['main__text']}>
        <h3 className={classes['main__title']}>{title}</h3>
        <p className={classes['main__author']}>by Valdemaras</p>
      </div>
    </li>
  );
};

export default MainSound;

为什么Redux也出同样问题?

因为你在Redux里也用了单一的布尔值isPlaying,和Context的逻辑完全一致,改成记录「当前播放音频ID」的结构就能解决。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 22:07:54