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

使用filter删除数组最后一项时React组件未重渲染问题

问题现象
  • 在reducer中使用filter方法实现用户点击时从localStorage删除对应电影的逻辑,测试发现除最后一项外,其余电影均可正常从localStorage移除
  • 最后一项虽会从状态中删除,但不会同步更新到localStorage
  • 初步排查怀疑诱因是删除状态中最后一项时组件未触发重渲染,导致对应的useEffect钩子未执行
  • 该bug仅出现在/watched路由下,处于根路由/时,可正常删除localStorage中的所有条目
相关代码

MoviesContext.js

const initialState = {
    movies: [],
    favoriteMovies: JSON.parse(localStorage.getItem('favorite')) || [],
    watchedMovies: JSON.parse(localStorage.getItem('watched')) || [],
}

MoviesReducer.js

case 'DELETE_MOVIE_FROM_WATCHED':
    return {
        ...state,
        watchedMovies: state.watchedMovies.filter(
            item => item.id !== action.payload
        ),
    }

useMovies.js

const { favoriteMovies, watchedMovies, dispatch } = useContext(MoviesContext)

const deleteMovieFromLS = async (id) => {
    dispatch({ type: 'DELETE_MOVIE_FROM_WATCHED', payload: id })
}

useEffect(() => {
    localStorage.setItem('watched', JSON.stringify(watchedMovies))
}, [watchedMovies])
问题根因

问题和reducer的filter逻辑无关,filter始终返回新数组,状态引用变化完全符合React重渲染规则。
真正原因是:写有localStorage同步逻辑的useEffect所在组件,在删除最后一项电影时被提前卸载了。
通常/watched路由下的页面会内置「列表为空时显示空状态/跳转其他页面/卸载列表组件」的逻辑,当删除最后一项后,watchedMovies变为空数组,触发对应组件/页面卸载,此时useEffect还没来得及响应watchedMovies的变化执行写入回调,就随组件一起被销毁,最终空数组的状态没有写入localStorage。
根路由下承载同步逻辑的组件始终处于挂载状态,不会因为列表变空被卸载,所以useEffect每次都能正常响应状态变化执行写入,删除功能表现正常。

修复方案

方案一(推荐):将同步localStorage的逻辑上移到常驻组件

把useEffect同步逻辑移到不会随路由切换/列表状态变化卸载的顶层位置,比如MoviesContext的Provider组件内部,保证只要状态变化,同步逻辑就一定能执行:

// 在MoviesContext的Provider组件中编写同步逻辑
const [state, dispatch] = useReducer(moviesReducer, initialState)

// 同步已观看列表
useEffect(() => {
  localStorage.setItem('watched', JSON.stringify(state.watchedMovies))
}, [state.watchedMovies])

// 同步收藏列表
useEffect(() => {
  localStorage.setItem('favorite', JSON.stringify(state.favoriteMovies))
}, [state.favoriteMovies])

这种写法从根源上避免了组件卸载导致逻辑不执行的问题,也符合状态和状态同步逻辑就近维护的原则。

方案二:删除时主动同步localStorage

修改删除方法,不依赖useEffect触发写入,删除动作触发时主动计算新列表并同步到localStorage:

const deleteMovieFromLS = (id) => {
    const updatedWatchedList = watchedMovies.filter(item => item.id !== id)
    localStorage.setItem('watched', JSON.stringify(updatedWatchedList))
    dispatch({ type: 'DELETE_MOVIE_FROM_WATCHED', payload: id })
}

这种写法更直接,但如果后续有其他修改watchedMovies的动作,需要每个操作位置都手动加同步逻辑,维护成本更高。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 13:36:14