首页 > 解决方案 > Jotai + React:意外的重新渲染和意外的陈旧状态值

问题描述

我正在制作一款游戏,与大多数游戏一样,它具有可以从游戏中的其他任何地方触发的各种场景。

我选择使用 Jotai,因为我发现它非常容易用作全局状态管理工具。Jotai 原子的行为有点像setStateuseContext组合。

引导

我将玩家的能量存储在一个原子中。当能量达到0时,组件useEffect中的<EnergyLevel />a 会触发“全局事件状态”原子的变化。更改被父组件拾取,如果全局事件状态值是“结束游戏”值<Game />,则有条件地呈现组件。<EndGame />

用户可以单击“重新开始”,这会重置全局事件状态值和全局“活动屏幕”值,这会触发主菜单呈现。此时,全局事件状态已显式设置为NONE

当用户点击“加载最后一场比赛”——它设置了一个“加载状态”原子——父<Game />组件重新渲染并useEffect有条件地调用一个派生的、只写的原子,该原子从数据库中获取最后保存的游戏并设置所有其他原子到这些值。它还将任何全局状态跟踪原子重置为新加载游戏的默认值。

错误

从技术上讲,到目前为止,所有内容实际上都按预期呈现,但对于一个额外的组件:组件在完成加载后再次<EndGame />呈现。<Game />

进一步研究(以及许多 console.logs),我发现在预期的最终渲染之后(在 useEffect 之后)触发了另一个渲染并且<Game />在那个意外的阶段,全局事件原子值,显式设置NONE为以某种方式重置为旧的事件值。

问题

正如我在上面的错误部分的第二段中提到的,是什么导致了额外的渲染周期?为什么eventTriggeredOfType在这个意想不到的渲染过程中原子值会发生变化?

编码

具有这种奇怪行为的可重现代码在这里被沙盒化。加载后只需按照说明进行操作。

以下是一些片段来演示预期的逻辑:

// here's the function that decreases playerEnergy, which is triggered by a button
// Game.tsx
const [playerEnergy, setPlayerEnergy] = useAtom(playerEnergyAtom);
const decreaseEnergy = () => {
   setPlayerEnergy(playerEnergy <= 0 ? 0 : playerEnergy - 10);
};


// then I watch playerEnergy and set the an event state to NO_ENERGY if it is 0
// EnergyLevel.tsx
const [, setEventTriggeredOfType] = useAtom(eventTriggeredOfTypeAtom);
useEffect(() => {
  if (playerEnergy <= 0) setEventTriggeredOfType(EventType.NO_ENERGY);
}, [playerEnergy, setEventTriggeredOfType]);


// here is the EndGame component that is only rendered when
// shouldTriggerEndGame returns true
// Game.tsx
const [shouldTriggerEndGame] = useAtom(shouldTriggerEndGameAction);
if (shouldTriggerEndGame) return <EndGame />;


// here is the shouldTriggerEndGame atom that checks the event state
// gameActions.ts (this file holds all the atoms)
export const shouldTriggerEndGameAction = atom((get) => {
  // this is what returns stale atom value SOMETIMES
  const eventTriggeredOfType = get(eventTriggeredOfTypeAtom); 
  return [
    eventTriggeredOfType === EventType.NO_ENERGY
  ].some((triggerState: boolean): boolean => triggerState === true);
});


// here is the "Start Over" button handler that resets the EventType above:
// EndGame.tsx
const goBackToMainMenu = () => {
    setEventTriggeredOfType(EventType.NONE);
    // triggers a return to the main screen (always works)
    setActiveScreen(Screen.NONE);
};


// these are triggered by UI buttons from the user
// gameActions.ts
export const startNewGameAction = atom(null, (_get, set) => {
  set(resetDefaultGameState, null);
  set(isLoadingGameOfTypeAtom, LoadType.NEW);
});

export const loadLastGameAction = atom(null, (_get, set) => {
  set(resetDefaultGameState, null);
  set(isLoadingGameOfTypeAtom, LoadType.SAVED);
});


// isLoadingGameOfType is being watched from within <Game />
// Game.tsx
const loadSavedGameRef = useRef(loadSavedGame);
const createNewGameRef = useRef(createNewGame);
useEffect(() => {
  if (isLoadingGameOfType === LoadType.SAVED) loadSavedGameRef.current();
  if (isLoadingGameOfType === LoadType.NEW) createNewGameRef.current();
}, [isLoadingGameOfType]);


// these are triggered by <Game />'s useEffects when the appropriate
// eventTriggeredOfType values are true
// gameActions.ts
export const loadSavedGameAction = atom(null, async (get, set) => {
  const playerData = get(playerDataAtom);
  set(playerEnergyAtom, playerData.lastGameState.playerEnergy);
});

export const createNewGameAction = atom(null, (_get, set) => {
  set(playerEnergyAtom, 100);
});

标签: reactjsreact-state-managementreact-lifecyclereact-lifecycle-hooks

解决方案


推荐阅读