React JS中dispatch Redux store action后页面白屏问题排查
YouTube风格点赞点踩功能点击按钮白屏问题修复
问题背景
实现类YouTube的影视内容点赞/点踩计量功能,从独立JS文件读取模拟电影数据,通过Redux管理状态,绑定点赞点踩按钮交互后,点击按钮页面直接空白无响应。
涉及的原始代码
movie.js(模拟数据源)
const movies = [ { id: '1', title: 'Oceans 8', category: 'Comedy', likes: 4, dislikes: 1 }, { id: '2', title: 'Midnight Sun', category: 'Comedy', likes: 2, dislikes: 0 }, { id: '3', title: 'Les indestructibles 2', category: 'Animation', likes: 3, dislikes: 1 }, ] export const movies$ = new Promise((resolve, reject) => setTimeout(resolve, 100, movies))
Reducer.js(原始版本,存在错误)
// 原始代码首行笔误:onst -> const onst initialState = { allMovies: [], filteredMovies: [], }; const Reducer = (state = initialState, action) => { switch (action.type) { case 'initial': return { ...state, allMovies: action.data, }; case 'remove': const removedMovie = state.allMovies.filter((movie) => movie.id !== action.id) return { ...state, allMovies: removedMovie }; case 'likes': // 核心错误:state是对象,不是数组,直接调用map会报错 return state.map((movie) => { if (movie.id === action.id) { if (movie.dislikesActive) { return { ...movie, likes: movie.likes + 1, dislikes: movie.dislikes - 1, likesActive: !movie.likesActive, dislikesActive: movie.likesActive }; } return { ...movie, likes: movie.likes + 1, likesActive: !movie.likesActive, dislikesActive: movie.likesActive }; } return movie; }); case 'dislikes': // 核心错误:state是对象,不是数组,直接调用map会报错 return state.map((movie) => { if (movie.id === action.id) { if (movie.likesActive) { return { ...movie, dislikes: movie.dislikes + 1, likes: movie.likes -1, dislikesActive: !movie.dislikesActive, likesActive: movie.dislikesActive }; } return { ...movie, dislikes: movie.dislikes + 1, dislikesActive: !movie.dislikesActive, likesActive: movie.dislikesActive }; } return movie; }); case 'filter': const filteredMovies = state.allMovies.filter(movie => movie.category === action.category); if (action.category) { return { ...state, filteredMovies }; } else { return { ...state } } default: return state; } }; export default Reducer;
store.js(原始版本,存在错误)
import Reducer from './Redux/Reducer'; // 原始代码漏导入configureStore export default configureStore({ reducer: { Reducer }, });
Listing.jsx(业务组件,原始版本存在错误)
import React, {useEffect, useReducer, useState} from "react"; import IconButton from '@mui/material/IconButton'; import DeleteIcon from '@mui/icons-material/Delete'; import ThumbUpOutlinedIcon from '@mui/icons-material/ThumbUpOutlined'; import ThumbDownOutlinedIcon from '@mui/icons-material/ThumbDownOutlined'; import { red } from '@mui/material/colors'; import Reducer from "../../Redux/Reducer"; import Category from "../Category/Category"; import Paginations from "../Pagination/Paginations"; import {movies$} from "../../Services/movies"; import "../../Styles/Components/MoviesCards/Listing.css" const titleStyle = { textAlign: "left", fontSize: "5vw", fontFamily: "Mont", color: "#283D47", }; const Listing = () => { // 错误点:配置了Redux却用组件内useReducer,状态管理脱节 const [state, dispatch] = useReducer(Reducer, {allMovies: [], filteredMovies: []}); const [activePage, setCurrentPage] = useState(1); const [moviesPerPage, setMoviesPerPage] = useState(4); const filteredCategory = Array.from(new Set(state.allMovies.map(movie => movie.category))); const indexOfLastMovie = activePage * moviesPerPage; const indexOfFirstMovie = indexOfLastMovie - moviesPerPage; const currentMovies = state.filteredMovies && state.filteredMovies.length !== 0 ? state.filteredMovies.slice( indexOfFirstMovie, indexOfLastMovie ) : state.allMovies.slice( indexOfFirstMovie, indexOfLastMovie ); const handlePageChange = (pageNumber) => { setCurrentPage(pageNumber) }; useEffect(() => { const fetchMovies = async () => { return movies$; }; // 错误点:初始数据没补likesActive/dislikesActive默认值 fetchMovies().then(data => dispatch({type:'initial', data})); }, []); return ( <> <Category filteredCategory={filteredCategory} dispatch={dispatch} /> <div className="cardsComponent"> {state && currentMovies.map(movie => { return ( <div className="movieCard"> <div key={movie.id}> <div className="topContainer"> <IconButton onClick={() => dispatch({type:'remove', id: movie.id})} value={2} aria-label="delete" size="large"> <DeleteIcon fontSize="inherit" sx={{ color: red[500] }}/> </IconButton> </div> <div className="citiesPricesContainer"> <div className="bottomContainer"> <span className="movieName">{movie.title}</span> </div> <div className="bottomContainer"> <span className="currentPrice">{movie.category}</span> </div> <div className="bottomContainer"> <IconButton active={movie.likesActive} onClick={() => dispatch({ type:'likes', id: movie.id})} value={2}> <ThumbUpOutlinedIcon fontSize="inherit" /> </IconButton> <p>{movie.likes}</p> <IconButton active={movie.dislikesActive} onClick={() => dispatch({ type:'dislikes', id: movie.id})} value={2}> <ThumbDownOutlinedIcon fontSize="inherit" /> </IconButton> <p>{movie.dislikes}</p> </div> </div> </div> </div> ) }) } </div> <div className="footer"> <Paginations state={state.filteredMovies && state.filteredMovies.length !== 0 ? state.filteredMovies : state.allMovies} activePage={activePage} handlePageChange={handlePageChange} handleNumberMovies={setMoviesPerPage}/> </div> </> ) } export default Listing;
问题根因
点击按钮白屏是JS运行时报错导致React渲染树崩溃,核心错误共3处:
- 最致命错误:Reducer中处理
likes/dislikes动作时,直接对整个state对象调用数组方法.map()。你的state结构是{allMovies: [], filteredMovies: []}的普通对象,本身没有map方法,触发TypeError: state.map is not a function直接打崩页面。 - 架构错误:写了Redux store配置但完全没在组件中对接,反而用组件内置
useReducer维护本地状态,两套状态逻辑完全脱节;同时store.js漏了configureStore的导入语句,store本身就无法正常初始化。 - 细节错误:Reducer首行
const笔误写成onst触发语法错误;初始加载的电影数据没有likesActive/dislikesActive默认字段,第一次点击时访问undefined属性会触发异常;点赞点踩的激活状态切换逻辑写反,会导致互斥逻辑失效。
修复方案
1. 修复Reducer逻辑
修正语法笔误,将点赞/点踩的处理逻辑改为操作state.allMovies数组,修正状态切换逻辑,操作后同步更新筛选列表保证状态一致:
const initialState = { allMovies: [], filteredMovies: [], }; const Reducer = (state = initialState, action) => { switch (action.type) { case 'initial': // 初始化时给每个电影补默认的激活状态 const initMovies = action.data.map(movie => ({ ...movie, likesActive: false, dislikesActive: false })) return { ...state, allMovies: initMovies, filteredMovies: initMovies }; case 'remove': const afterRemove = state.allMovies.filter((movie) => movie.id !== action.id) return { ...state, allMovies: afterRemove, filteredMovies: state.filteredMovies.filter(movie => movie.id !== action.id) }; case 'likes': const afterLike = state.allMovies.map((movie) => { if (movie.id === action.id) { // 已经点过赞,再次点击取消赞 if (movie.likesActive) { return { ...movie, likes: movie.likes - 1, likesActive: false, }; } // 之前点过踩,先取消踩再加赞 if (movie.dislikesActive) { return { ...movie, likes: movie.likes + 1, dislikes: movie.dislikes - 1, likesActive: true, dislikesActive: false }; } // 普通点赞 return { ...movie, likes: movie.likes + 1, likesActive: true, }; } return movie; }); // 同步更新筛选后的列表 const filteredAfterLike = state.filteredMovies.length ? afterLike.filter(movie => state.filteredMovies.some(fm => fm.id === movie.id)) : [] return { ...state, allMovies: afterLike, filteredMovies: filteredAfterLike } case 'dislikes': const afterDislike = state.allMovies.map((movie) => { if (movie.id === action.id) { // 已经点过踩,再次点击取消踩 if (movie.dislikesActive) { return { ...movie, dislikes: movie.dislikes - 1, dislikesActive: false, }; } // 之前点过赞,先取消赞再加踩 if (movie.likesActive) { return { ...movie, dislikes: movie.dislikes + 1, likes: movie.likes -1, dislikesActive: true, likesActive: false }; } // 普通点踩 return { ...movie, dislikes: movie.dislikes + 1, dislikesActive: true, }; } return movie; }); // 同步更新筛选后的列表 const filteredAfterDislike = state.filteredMovies.length ? afterDislike.filter(movie => state.filteredMovies.some(fm => fm.id === movie.id)) : [] return { ...state, allMovies: afterDislike, filteredMovies: filteredAfterDislike } case 'filter': if (action.category) { return { ...state, filteredMovies: state.allMovies.filter(movie => movie.category === action.category) }; } else { return { ...state, filteredMovies: state.allMovies } } default: return state; } }; export default Reducer;
2. 修复store配置
补上缺失的导入:
import { configureStore } from '@reduxjs/toolkit'; import Reducer from './Redux/Reducer'; export default configureStore({ reducer: { movie: Reducer }, });
3. 修复组件对接Redux
移除组件内多余的useReducer,改用Redux钩子读取状态、派发动作,记得在应用根组件用Provider包裹传入store:
import React, {useEffect, useState} from "react"; import { useSelector, useDispatch } from 'react-redux'; import IconButton from '@mui/material/IconButton'; import DeleteIcon from '@mui/icons-material/Delete'; import ThumbUpOutlinedIcon from '@mui/icons-material/ThumbUpOutlined'; import ThumbDownOutlinedIcon from '@mui/icons-material/ThumbDownOutlined'; import { red } from '@mui/material/colors'; import Category from "../Category/Category"; import Paginations from "../Pagination/Paginations"; import {movies$} from "../../Services/movies"; import "../../Styles/Components/MoviesCards/Listing.css" const Listing = () => { const dispatch = useDispatch(); // 从Redux读取状态 const { allMovies, filteredMovies } = useSelector(state => state.movie); const [activePage, setCurrentPage] = useState(1); const [moviesPerPage, setMoviesPerPage] = useState(4); const filteredCategory = Array.from(new Set(allMovies.map(movie => movie.category))); const indexOfLastMovie = activePage * moviesPerPage; const indexOfFirstMovie = indexOfLastMovie - moviesPerPage; const currentMovies = filteredMovies.length !== 0 ? filteredMovies.slice( indexOfFirstMovie, indexOfLastMovie ) : allMovies.slice( indexOfFirstMovie, indexOfLastMovie ); const handlePageChange = (pageNumber) => { setCurrentPage(pageNumber) }; useEffect(() => { movies$.then(data => dispatch({type:'initial', data})); }, [dispatch]); return ( <> <Category filteredCategory={filteredCategory} dispatch={dispatch} /> <div className="cardsComponent"> {currentMovies.map(movie => { return ( <div className="movieCard" key={movie.id}> <div className="topContainer"> <IconButton onClick={() => dispatch({type:'remove', id: movie.id})} aria-label="delete" size="large"> <DeleteIcon fontSize="inherit" sx={{ color: red[500] }}/> </IconButton> </div> <div className="citiesPricesContainer"> <div className="bottomContainer"> <span className="movieName">{movie.title}</span> </div> <div className="bottomContainer"> <span className="currentPrice">{movie.category}</span> </div> <div className="bottomContainer"> <IconButton color={movie.likesActive ? "primary" : "default"} onClick={() => dispatch({ type:'likes', id: movie.id})}> <ThumbUpOutlinedIcon fontSize="inherit" /> </IconButton> <p>{movie.likes}</p> <IconButton color={movie.dislikesActive ? "error" : "default"} onClick={() => dispatch({ type:'dislikes', id: movie.id})}> <ThumbDownOutlinedIcon fontSize="inherit" /> </IconButton> <p>{movie.dislikes}</p> </div> </div> </div> ) })} </div> <div className="footer"> <Paginations state={filteredMovies.length !== 0 ? filteredMovies : allMovies} activePage={activePage} handlePageChange={handlePageChange} handleNumberMovies={setMoviesPerPage}/> </div> </> ) } export default Listing;
内容的提问来源于stack exchange,提问作者MD_Siyam
相关产品推荐
相关产品推荐

