我的状态在减速器和消费组件之间变化

Posted

技术标签:

【中文标题】我的状态在减速器和消费组件之间变化【英文标题】:My state changes between the reducer and the consuming component 【发布时间】:2021-11-17 12:27:38 【问题描述】:

应用目的:这个 React 应用的目的是处理一个非常具体的飞镖游戏的得分。 2 名球员,每人必须在 20-13、Tpl、Dbls 和公牛队中达到 33 次安打。没有积分,只计算命中数。命中由玩家手动添加(不需要自动化:))。 每个目标字段都有一行目标和 2 个按钮,用于添加和删除该目标字段的匹配项。

我已经实现了用于维护状态的 useContext-design,如下所示:

export interface IMickeyMouseGameState 
    player1 : IPlayer | null,
    player2 : IPlayer | null,
    winner : IPlayer | null,
    TotalRounds : number,
    GameStatus: Status
    CurrentRound: number

其他对象是这样设计的:

export interface IGame 
    player1?:IPlayer;
    player2?:IPlayer;
    sets: number;   
    gameover:boolean;
    winner:IPlayer|undefined;

export interface IPlayer 
    id:number;
    name: string;
    targets: ITarget[];
    wonSets: number;
    hitRequirement : number

export interface ITarget 
    id:number,
    value:string,
    count:number


export interface IHit
    playerid:number,
    targetid:number

到目前为止一切顺利。

这是带有签名的reducer action:

export interface HitPlayerTarget 
    type: ActionType.HitPlayerTarget,
    payload:IHit


const newTargets = (action.payload.playerid === 1 ? [...state.player1!.targets] : [...state.player2!.targets]);

            const hitTarget = newTargets.find(tg => 
                return tg.id === action.payload.targetid;
            );
        
            if (hitTarget) 
                const newTarget = ...hitTarget
                newTarget.count = hitTarget.count-1;
        
                newTargets.splice(newTargets.indexOf(hitTarget),1);
                newTargets.push(newTarget);
            

            if (action.payload.playerid === 1) 
                state.player1!.targets = [...newTargets];
            

            if (action.payload.playerid === 2) 
                state.player2!.targets = [...newTargets];
            

            
            let newState: IMickeyMouseGameState = 
                ...state,
                player1: 
                    ...state.player1!,
                    targets: [...state.player1!.targets]
                ,
                player2: 
                    ...state.player2!,
                    targets: [...state.player2!.targets]
                
            
            return newState;

在 Main 组件中我实例化了 useReducerHook:

const MickeyMouse: React.FC = () => 

    const [state, dispatch] = useReducer(mickeyMousGameReducer, initialMickeyMouseGameState);

    const p1Props: IUserInputProps = 
        color: "green",
        placeholdertext: "Angiv Grøn spiller/hold",
        iconSize: 24,
        playerId: 1,
    

    const p2Props: IUserInputProps = 
        playerId: 2,
        color: "red",
        placeholdertext: "Angiv Rød spiller/hold",
        iconSize: 24,
    

    return (
        <MickyMouseContext.Provider value= state, dispatch  >

            <div className="row mt-3 mb-5">
                <h1 className="text-success text-center">Mickey Mouse Game</h1>
            </div>

            <MickeyMouseGameSettings />

            <div className="row justify-content-start">
                <div className="col-5">
                    state.player1 ?<UserTargetList playerid=1 /> : <UserInput ...p1Props /> 
                </div>
                <div className="col-1 bg-dark text-warning rounded border border-warning">
                    <MickeyMouseLegend />
                </div>
                <div className="col-5">
                    state.player2 ? <UserTargetList playerid=2 /> : <UserInput ...p2Props /> 
                </div>
            </div>

        </MickyMouseContext.Provider>
    );


export default MickeyMouse;


现在 reducer-action 正确地从目标的计数中减去 1(重点是让每个目标计数为 0,并且新状态正确显示目标比旧状态少 1,但是当消费者(在这种情况下)一个名为 UserTargets 的 tsx 组件,它负责用圆圈或 X 渲染每个目标)目标的状态低 2,即使减速器只减去 1....

在 20 场向玩家“彼得”添加单次命中后 - 渲染(带有控制台日志)如下所示:

所以我想我的问题是:为什么减速器和消费者之间的状态会发生变化,我能做些什么来解决它?

如果需要进一步解释,请询问,如果这个问题应该简化,请告诉我...... 我通常不会在这里提问 - 我主要是找到答案。

我在github上的项目:https://github.com/martinmoesby/dart-games

【问题讨论】:

您是否将您的应用程序渲染为React.StrictMode 组件?您能否更新问题以包括 useReducer 钩子的使用位置以及您向其发送的内容? state.player1!.targets = [...newTargets]; 似乎是一种状态突变。 应用处于严格模式。 我更新了问题以包括 useReducer 的实例化 【参考方案1】:

问题

我怀疑React.StrictMode 暴露了您的减速器案例中的状态突变。

StrictMode - Detecting unexpected side effects

严格模式无法自动为您检测副作用,但它 可以通过使它们更具确定性来帮助您发现它们。 这是通过有意双重调用以下函数来完成的:

类组件constructorrendershouldComponentUpdate方法 类组件静态getDerivedStateFromProps方法 函数组件体 状态更新函数(setState 的第一个参数) 函数传递给 useStateuseMemouseReducer

这个函数是reducer函数。

const newTargets = (action.payload.playerid === 1 // <-- new array reference OK
  ? [...state.player1!.targets]
  : [...state.player2!.targets]);

const hitTarget = newTargets.find(tg => 
  return tg.id === action.payload.targetid;
);
        
if (hitTarget) 
  const newTarget =  ...hitTarget ; // <-- new object reference OK
  newTarget.count = hitTarget.count - 1; // <-- new property OK
        
  newTargets.splice(newTargets.indexOf(hitTarget), 1); // <-- inplace mutation but OK since newTargets is new array
  newTargets.push(newTarget); // <-- same


if (action.payload.playerid === 1) 
  state.player1!.targets = [...newTargets]; // <-- state.player1!.targets mutation!


if (action.payload.playerid === 2) 
  state.player2!.targets = [...newTargets]; // <-- state.player2!.targets mutation!


let newState: IMickeyMouseGameState = 
  ...state,
  player1: 
    ...state.player1!,
    targets: [...state.player1!.targets] // <-- copies mutation
  ,
  player2: 
    ...state.player2!,
    targets: [...state.player2!.targets] // <-- copies mutation
  

return newState;

state.player1!.targets = [...newTargets]; 在更新中变异并复制到之前的state.player1 状态,当reducer 再次运行时,第二个更新再次变异并在更新中复制。

解决方案

应用不可变的更新模式。浅拷贝所有正在更新的状态。

const newTargets = (action.payload.playerid === 1
  ? [...state.player1!.targets]
  : [...state.player2!.targets]);

const hitTarget = newTargets.find(tg => tg.id === action.payload.targetid);
        
if (hitTarget) 
  const newTarget = 
    ...hitTarget,
    count: hitTarget.count - 1,
  ;
        
  newTargets.splice(newTargets.indexOf(hitTarget), 1);
  newTargets.push(newTarget);


const newState: IMickeyMouseGameState =  ...state ; // shallow copy

if (action.payload.playerid === 1) 
  newState.player1 = 
    ...newState.player1!, // shallow copy
    targets: newTargets,
  ;


if (action.payload.playerid === 2) 
  newState.player1 = 
    ...newState.player2!, // shallow copy
    targets: newTargets,
  ;


return newState;

【讨论】:

该死-我怎么没看到!!!所以我的问题是混合变异和非变异状态....这很完美,非常感谢!!! @MartinFrankMoesbyPetersen IDK,有时很难找到对象突变,特别是如果您不熟悉更新向量,或者您只是对代码视而不见,需要对代码进行第二次观察。我想在你看过足够多的时间之后,你会更好地直觉到哪里看。您至少包含了相关代码,所以谢谢您。 ? 干杯,祝你好运。

以上是关于我的状态在减速器和消费组件之间变化的主要内容,如果未能解决你的问题,请参考以下文章

不使用突变直接改变 Vuex 状态

为啥即使我没有将 vuex 状态绑定到任何 html 输入元素,我的 vuex 状态也会在更改组件级别状态时发生变化?

kafka原理分析

尽管状态发生了变化,但自定义钩子不会触发组件重新渲染

观察者模式

react监听仓库数据变化