使用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
相关产品推荐
相关产品推荐

