我的状态在 reducer 和消费组件之间变化
My state changes between the reducer and the consuming component
应用程序用途:此 React 应用程序的用途是处理非常具体的飞镖游戏的计分。 2 名球员,每名球员必须在 20-13 场、Tpl、Dbls 和 Bulls 中达到 33 次安打。没有分数,只计算命中次数。命中由玩家手动添加(不需要自动化 :))。
每个目标字段都有一行目标和 2 个按钮,用于添加和删除该目标字段的命中。
我已经实现了用于维护状态的 useContext 设计,如下所示:
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
}
到目前为止一切顺利。
这是带有签名的减速器操作:
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;
在主要组件中,我实例化了 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,即使 reducer 只减去 1....
在 20 场中向玩家 'Peter' 添加单次命中后 - 渲染(带有控制台日志)如下所示:
所以我想我的问题是:为什么 reducer 和消费者之间的状态会发生变化,我该如何解决它?
如果需要进一步的解释,请询问,如果这个问题应该被简化,请让我知道...
我通常不会在这里提问 - 我主要是在寻找答案。
我在 github 上可用的项目:https://github.com/martinmoesby/dart-games
问题
我怀疑 React.StrictMode
.
暴露了你的 reducer 案例中的状态突变
StrictMode - Detecting unexpected side effects
Strict mode can’t automatically detect side effects for you, but it
can help you spot them by making them a little more deterministic.
This is done by intentionally double-invoking the following functions:
- Class component
constructor
, render
, and shouldComponentUpdate
methods
- Class component static
getDerivedStateFromProps
method
- Function component bodies
- State updater functions (the first argument to
setState
)
- Functions passed to
useState
, useMemo
, or useReducer
<--
函数是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
状态,当减速器再次为 运行 时,第二个更新再次变异并复制到更新中。
解决方案
应用不可变的更新模式。浅复制所有状态正在更新。
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;
应用程序用途:此 React 应用程序的用途是处理非常具体的飞镖游戏的计分。 2 名球员,每名球员必须在 20-13 场、Tpl、Dbls 和 Bulls 中达到 33 次安打。没有分数,只计算命中次数。命中由玩家手动添加(不需要自动化 :))。 每个目标字段都有一行目标和 2 个按钮,用于添加和删除该目标字段的命中。
我已经实现了用于维护状态的 useContext 设计,如下所示:
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
}
到目前为止一切顺利。
这是带有签名的减速器操作:
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;
在主要组件中,我实例化了 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,即使 reducer 只减去 1....
在 20 场中向玩家 'Peter' 添加单次命中后 - 渲染(带有控制台日志)如下所示:
所以我想我的问题是:为什么 reducer 和消费者之间的状态会发生变化,我该如何解决它?
如果需要进一步的解释,请询问,如果这个问题应该被简化,请让我知道... 我通常不会在这里提问 - 我主要是在寻找答案。
我在 github 上可用的项目:https://github.com/martinmoesby/dart-games
问题
我怀疑 React.StrictMode
.
StrictMode - Detecting unexpected side effects
Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions:
- Class component
constructor
,render
, andshouldComponentUpdate
methods- Class component static
getDerivedStateFromProps
method- Function component bodies
- State updater functions (the first argument to
setState
)- Functions passed to
useState
,useMemo
, oruseReducer
<--
函数是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
状态,当减速器再次为 运行 时,第二个更新再次变异并复制到更新中。
解决方案
应用不可变的更新模式。浅复制所有状态正在更新。
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;