私は、ほとんどのゲームと同様に、ゲーム内のどこからでもトリガーできるさまざまなシナリオを持つゲームを作成しています。
グローバルな状態管理ツールとして非常に使いやすい Jotai を使用することにしました。Jotai 原子は少し似たような振る舞いをしsetState
、useContext
結合します。
リードアップ
プレイヤーのエネルギーをアトムに蓄えています。エネルギーが に達する0
とuseEffect
、<EnergyLevel />
コンポーネント内の a が「グローバル イベント状態」原子の変化を引き起こします。その変更は親コンポーネントに反映され、グローバル イベント状態値が「ゲーム終了」値である場合、<Game />
条件付きでコンポーネントがレンダリングされます。<EndGame />
ユーザーは「やり直す」をクリックすると、グローバル イベント状態値とグローバル「アクティブ スクリーン」値がリセットされ、メイン メニューのレンダリングがトリガーされます。この時点で、グローバル イベント状態は明示的に に設定されていNONE
ます。
ユーザーが "Load Last Game" をクリックすると ("loading state" アトムが設定されます)、親<Game />
コンポーネントが再レンダリングされ、useEffect
条件付きで、データベースから最後に保存されたゲームを取得して設定する、派生した書き込み専用アトムが呼び出されます。他のすべてのアトムをそれらの値にします。また、新しくロードされたゲームのデフォルトにアトムを追跡するグローバル状態をリセットします。
不具合
技術的には、この時点までは実際にはすべてが期待どおりにレンダリングされますが、1 つの追加コンポーネント: の読み込みが完了した後、コンポーネントが再度<EndGame />
レンダリングされます。<Game />
これをさらに (そして多くの console.logs を使用して) 調べたところ、別のレンダリングがの予想される最終レンダリング (useEffect の後) の後にトリガーされ、<Game />
その予期しないフェーズ中に、明示的に に設定されたグローバル イベント アトム値が であることがNONE
わかりました。どういうわけか古いイベント値にリセットします。
質問
上記の「バグ」セクションの 2 番目の段落で参照したように、追加のレンダリング サイクルの原因は何ですか? そして、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);
});