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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 00:36:15