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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 19:07:03