React动漫项目localStorage数据串扰问题求助
问题分析与修复方案
核心问题
你遇到的状态泄漏,本质是React组件状态没有随props变化同步更新:
useState的初始值仅在组件首次挂载时执行一次,当切换动漫(titleprops变化),组件重新渲染时,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的繁琐和潜在问题:
- 安装依赖:
npm install @reduxjs/toolkit-persist - 创建一个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;
- 在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);
- 在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
相关产品推荐
相关产品推荐

