React Context已更新但组件未渲染的问题排查
问题描述
我在公司项目中尝试结合Hooks使用Context API,采用TypeScript开发。创建了Context、Provider和Reducer后,点击按钮时状态已变更,但组件未渲染这些变化。我制作了简化复现版本仍未找到问题,请问我哪里出错了?
App.tsx
import './App.css'; import { PlayerGameDataProvider } from './State/UserContext'; import GamesList from './Components/GamesList'; function App() { return ( <PlayerGameDataProvider> <div className="App"> <header className="App-header"> <GamesList /> </header> </div> </PlayerGameDataProvider> ); } export default App;
UserContext.tsx
import { createContext, useReducer, Dispatch } from "react" export enum ActionType { Accept = 'ACCEPTED', Reject = 'REJECTED', } export enum ResponseState { Accepted = 'ACCEPTED', Rejected = 'REJECTED', Pending = 'PENDING', } export enum GameState { Ongoing = 'ONGOING', Upcoming = 'UPCOMING', Finished = 'FINISHED', } export interface IGameStatus { id: number, name:string, response: ResponseState, state: GameState, } export interface IPlayerResponseData { type: ActionType, payload: { tournamentId: number }, } export interface IPlayerGameData { playerId: number, games: IGameStatus[], } // 修正:正确创建新数组并修改对应元素,保持不可变性 const setPlayerResponse = (state: IPlayerGameData, action: IPlayerResponseData): IGameStatus[] => { // 复制原games数组,遍历修改匹配的元素 return state.games.map(game => { if (game.id === action.payload.tournamentId) { // 返回新对象,不修改原对象 return { ...game, response: action.type === ActionType.Accept ? ResponseState.Accepted : ResponseState.Rejected }; } return game; }); } export const PlayerTournamentResponseAction = (state: IPlayerGameData, action: IPlayerResponseData): IPlayerGameData => { switch (action.type) { case ActionType.Accept: case ActionType.Reject: return { ...state, games: setPlayerResponse(state, action), } default: return state } } const InitGame = ():IPlayerGameData => { return { playerId: 12345, games: [ { id:0, name: "Game 1", response: ResponseState.Accepted, state: GameState.Ongoing, }, { id:1, name: "Game 2", response: ResponseState.Pending, state: GameState.Ongoing, }, { id:2, name: "Game 3", response: ResponseState.Pending, state: GameState.Ongoing, }, { id:3, name: "Game 4", response: ResponseState.Pending, state: GameState.Upcoming, }, { id:4, name: "Game 5", response: ResponseState.Pending, state: GameState.Upcoming, }, { id:5, name: "Game 6", response: ResponseState.Rejected, state: GameState.Finished, }, { id:6, name: "Game 7", response: ResponseState.Accepted, state: GameState.Finished, } ] } } const initialData:IPlayerGameData = InitGame(); // 修正:Context类型定义为[状态, dispatch]元组 export type PlayerContextType = [IPlayerGameData, Dispatch<IPlayerResponseData>]; export const PlayerContext = createContext<PlayerContextType>([initialData, () => {}]); export const PlayerGameDataProvider = (props: { children: React.ReactNode }) => { const [data, setData] = useReducer(PlayerTournamentResponseAction, initialData); return <PlayerContext.Provider value={[data, setData]}>{props.children}</PlayerContext.Provider> }
GamesList.tsx
import { useContext } from "react"; import { PlayerContext, IGameStatus } from "../State/UserContext"; import GameElement from "./GameElement"; const GamesList = () => { const [data] = useContext(PlayerContext); const gameListObjects = data.games.map((game: IGameStatus) => { return <GameElement key={game.id} // 补充key,避免React警告 id={game.id} name={game.name} state={game.state} response={game.response} />; }); return ( <div> {gameListObjects} </div> ) } export default GamesList;
GameElement.tsx
import { useContext } from "react"; import { ActionType, PlayerContext, IPlayerResponseData } from "../State/UserContext"; const GameElement = (props: { id: number, name: string, state: string, response: string }) => { const [, setData] = useContext(PlayerContext); const handleAccept = () => { // 修正:使用dispatch分发action,触发状态更新 const action: IPlayerResponseData = { type: ActionType.Accept, payload: { tournamentId: props.id } }; setData(action); }; const handleReject = () => { const action: IPlayerResponseData = { type: ActionType.Reject, payload: { tournamentId: props.id } }; setData(action); }; return ( <div> <ul> <dt>GameID: {props.id}</dt> <dt>Game Name: {props.name}</dt> <dt>Game Status: {props.state}</dt> <dt>Player Response: {props.response}</dt> <dt> <button onClick={handleAccept}>Accept</button> <button onClick={handleReject}>Reject</button> </dt> </ul> </div> ) } export default GameElement;
问题根源及修正说明
1. 未正确触发状态更新
GameElement中直接调用PlayerTournamentResponseAction只是计算新状态,但不会通知React更新上下文状态。必须使用useReducer返回的dispatch函数(即代码中的setData)来分发action,这样React才会重新计算状态并触发组件渲染。
2. Reducer违反不可变原则且返回错误数据
原setPlayerResponse函数直接修改原数组中的对象,并且返回过滤后的单元素数组,导致games数组被错误替换。修正后使用map创建新数组,对匹配的游戏返回新对象,既保持不可变性,又保留所有游戏数据。
3. Context类型定义不匹配
原Context泛型定义为IPlayerGameData[],但实际提供的是[状态, dispatch]元组,导致类型不兼容。修正后定义了PlayerContextType元组类型,确保类型安全。
4. 补充React列表key
在GamesList的map循环中添加了key属性,符合React列表渲染的要求,避免控制台警告。
内容的提问来源于stack exchange,提问作者alicankoker
相关产品推荐
相关产品推荐

