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

React动漫项目localStorage数据串扰问题求助

问题分析与修复方案

核心问题

你遇到的状态泄漏,本质是React组件状态没有随props变化同步更新:

  • useState的初始值仅在组件首次挂载时执行一次,当切换动漫(title props变化),组件重新渲染时,watched和currentEpisode状态不会自动读取新动漫的localStorage数据,依然保留前一部动漫的旧状态。
  • 后续点击新动漫的剧集时,旧的watched数组被追加新剧集,导致数据混入。

具体修复步骤

1. 监听title变化,同步重置状态

添加useEffect监听title和episodes的变化,每次切换动漫时,重新从localStorage读取对应数据并更新状态。

2. 修正localStorage读取的默认值逻辑

确保当localStorage没有对应动漫的记录时,默认使用当前动漫的第一集ID,而不是固定的旧值。

修改后的完整代码

import React from "react";
import { useGetAnimeEpisodeQuery } from "../../services/animeApi";
import { AnimeInfo } from "../../services/types/AnimeInfo";

interface PlayerProps extends AnimeInfo {}

const Player: React.FC<PlayerProps> = ({ image, episodes, title }) => {
  const [completed, setCompleted] = React.useState(false);
  const [watched, setWatched] = React.useState<string[]>([]);
  const [currentEpisode, setCurrentEpisode] = React.useState<string>(episodes[0].id);

  // 初始化&同步localStorage数据
  React.useEffect(() => {
    const storageKeyLast = `${title}_LAST_EPISODE`;
    const storageKeyWatched = `${title}_WATCHED_EPISODE`;
    
    // 处理上次观看剧集
    const savedLastEpisode = localStorage.getItem(storageKeyLast);
    const initialLastEpisode = savedLastEpisode ? JSON.parse(savedLastEpisode) : episodes[0].id;
    setCurrentEpisode(initialLastEpisode);
    
    // 处理已观看剧集
    const savedWatched = localStorage.getItem(storageKeyWatched);
    const initialWatched = savedWatched ? JSON.parse(savedWatched) : [episodes[0].id];
    setWatched(initialWatched);

    // 首次初始化时写入默认值
    if (!savedLastEpisode) {
      localStorage.setItem(storageKeyLast, JSON.stringify(initialLastEpisode));
    }
    if (!savedWatched) {
      localStorage.setItem(storageKeyWatched, JSON.stringify(initialWatched));
    }
  }, [title, episodes]);

  const { data: seria, isLoading, isSuccess, error } = useGetAnimeEpisodeQuery(currentEpisode);
  const episodeUrl = seria ? seria.headers.Referer : "";

  const setEpisode = (episodeId: string) => {
    setCurrentEpisode(episodeId);
    localStorage.setItem(`${title}_LAST_EPISODE`, JSON.stringify(episodeId));
    
    if (!watched.includes(episodeId)) {
      setWatched((prev) => [episodeId, ...prev]);
    }
  };

  React.useEffect(() => {
    if (watched.length > 0) {
      localStorage.setItem(`${title}_WATCHED_EPISODE`, JSON.stringify(watched));
    }
  }, [watched, title]);

  React.useEffect(() => {
    const checkHandler = () => {
      setTimeout(() => setCompleted(true), 5000);
    };
    isSuccess || isLoading ? checkHandler() : setCompleted(false);
  }, [isSuccess, isLoading]);

  if (error) return <div>请求失败</div>;

  return (
    <div className="mb-20">
      <div className="w-full h-[576px] full-bl relative bg-black overflow-hidden mx-auto">
        <div
          style={{ "--anime-bg-url": `url(${image})` } as React.CSSProperties}
          className={`relative w-full h-full bg-black z-10 after:bg-[image:--anime-bg-url] scale-110 after:bg-no-repeat after:bg-cover top-0 right-0 after:bg-center blur-xl brightness-50 ${
            completed && "hidden"
          } after:absolute after:inset-8 after:top-0 after:right-0 after:w-full after:h-full after:content-[''] after:bg-black after:z-[20]`}
        ></div>
        <iframe
          allow="autoplay"
          allowFullScreen
          scrolling="no"
          src={episodeUrl}
          className={`w-full h-full bg-black bg-[image:--anime-bg-url] bg-no-repeat bg-cover bg-center ${
            !completed && "blur-xl brightness-50"
          } flex items-center justify-center`}
        ></iframe>
        {!completed && <span className="loading loading-dots text-info absolute w-24 spinnerPos z-10"></span>}
      </div>
      <ul className="flex gap-3 flex-wrap px-2 py-2 max-h-48 overflow-y-auto bg-slate-900">
        {episodes.map((episode) => (
          <li
            onClick={() => setEpisode(episode.id)}
            className={`${
              watched.includes(episode.id) &&
              episode.id !== currentEpisode &&
              "opacity-50 bg-indigo-600 hover:bg-indigo-700"
            } ${
              episode.id === currentEpisode && "bg-indigo-600 hover:bg-indigo-700"
            } text-white btn-square btn text-lg w-12 border-none`}
            key={episode.id}
          >
            {episode.number}
          </li>
        ))}
      </ul>
    </div>
  );
};

export default Player;

关键修改点说明

  • 将状态初始化逻辑移到useEffect中,监听title和episodes,确保每次切换动漫时重新读取对应localStorage数据。
  • 移除了组件顶部直接读取localStorage并赋值给state的逻辑,避免初始值只执行一次的问题。
  • 给watched的localStorage写入逻辑添加了title依赖,确保写入的是当前动漫的存储键。

进阶替代方案:用RTK持久化管理状态

既然你已经在用Redux Toolkit,可以用@reduxjs/toolkit-persist将观看记录状态持久化到localStorage,避免直接操作localStorage的繁琐和潜在问题:

  1. 安装依赖:npm install @reduxjs/toolkit-persist
  2. 创建一个slice管理观看记录:
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { persistReducer } from "redux-persist";
import storage from "redux-persist/lib/storage"; // 默认用localStorage

interface WatchRecordState {
  [animeTitle: string]: {
    lastEpisode: string;
    watchedEpisodes: string[];
  };
}

const initialState: WatchRecordState = {};

const watchRecordSlice = createSlice({
  name: "watchRecords",
  initialState,
  reducers: {
    updateLastEpisode: (state, action: PayloadAction<{ title: string; episodeId: string }>) => {
      const { title, episodeId } = action.payload;
      if (!state[title]) {
        state[title] = { lastEpisode: episodeId, watchedEpisodes: [episodeId] };
      } else {
        state[title].lastEpisode = episodeId;
      }
    },
    addWatchedEpisode: (state, action: PayloadAction<{ title: string; episodeId: string }>) => {
      const { title, episodeId } = action.payload;
      if (!state[title]) {
        state[title] = { lastEpisode: episodeId, watchedEpisodes: [episodeId] };
      } else if (!state[title].watchedEpisodes.includes(episodeId)) {
        state[title].watchedEpisodes.unshift(episodeId);
      }
    },
  },
});

const persistConfig = {
  key: "watchRecords",
  storage,
};

export const persistedWatchRecordReducer = persistReducer(persistConfig, watchRecordSlice.reducer);
export const { updateLastEpisode, addWatchedEpisode } = watchRecordSlice.actions;
  1. 在store配置中集成persist:
import { configureStore } from "@reduxjs/toolkit";
import { persistStore } from "redux-persist";
import { persistedWatchRecordReducer } from "./slices/watchRecordSlice";
// 其他reducer...

export const store = configureStore({
  reducer: {
    watchRecords: persistedWatchRecordReducer,
    // 其他reducer...
  },
});

export const persistor = persistStore(store);
  1. 在Player组件中使用:
import { useDispatch, useSelector } from "react-redux";
import { updateLastEpisode, addWatchedEpisode } from "../../store/slices/watchRecordSlice";

// ...

const dispatch = useDispatch();
const watchRecord = useSelector((state: RootState) => state.watchRecords[title]);

// 初始化状态
React.useEffect(() => {
  if (!watchRecord) {
    dispatch(updateLastEpisode({ title, episodeId: episodes[0].id }));
  }
}, [title, episodes, watchRecord, dispatch]);

const setEpisode = (episodeId: string) => {
  setCurrentEpisode(episodeId);
  dispatch(updateLastEpisode({ title, episodeId }));
  dispatch(addWatchedEpisode({ title, episodeId }));
};

// 从redux读取状态
const currentEpisode = watchRecord?.lastEpisode || episodes[0].id;
const watched = watchRecord?.watchedEpisodes || [episodes[0].id];

这种方式更符合RTK的最佳实践,状态管理更集中,也避免了直接操作localStorage的错误。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 09:58:10