From 3ba5ea819f87829ab3f92d7b78773134b0349722 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 4 Nov 2024 10:44:10 +0100 Subject: [PATCH] Fix intempestive rerenders --- client/src/dojo/contractModels.ts | 427 +++++++++++++++-------------- client/src/dojo/setup.ts | 2 +- client/src/hooks/useChest.tsx | 3 +- client/src/hooks/useCredits.tsx | 3 +- client/src/hooks/useDeepMemo.tsx | 58 ++++ client/src/hooks/useGame.tsx | 3 +- client/src/hooks/useGrid.tsx | 4 + client/src/hooks/usePlayer.tsx | 3 +- client/src/hooks/useSettings.tsx | 3 +- client/src/hooks/useTournament.tsx | 2 + client/src/main.tsx | 5 +- client/src/ui/screens/Home.tsx | 8 +- 12 files changed, 301 insertions(+), 220 deletions(-) create mode 100644 client/src/hooks/useDeepMemo.tsx diff --git a/client/src/dojo/contractModels.ts b/client/src/dojo/contractModels.ts index fafce67c..1a0d106e 100644 --- a/client/src/dojo/contractModels.ts +++ b/client/src/dojo/contractModels.ts @@ -1,215 +1,230 @@ import { defineComponent, Type as RecsType, World } from "@dojoengine/recs"; - export type ContractComponents = Awaited< ReturnType >; export function defineContractComponents(world: World) { return { - Game: defineComponent( - world, - { - id: RecsType.Number, - over: RecsType.Boolean, - score: RecsType.Number, - moves: RecsType.Number, - next_row: RecsType.Number, - hammer_bonus: RecsType.Number, - wave_bonus: RecsType.Number, - totem_bonus: RecsType.Number, - hammer_used: RecsType.Number, - wave_used: RecsType.Number, - totem_used: RecsType.Number, - combo_counter: RecsType.Number, - max_combo: RecsType.Number, - blocks: RecsType.BigInt, - player_id: RecsType.BigInt, - seed: RecsType.BigInt, - mode: RecsType.Number, - start_time: RecsType.Number, - score_in_tournament: RecsType.Number, - combo_counter_in_tournament: RecsType.Number, - max_combo_in_tournament: RecsType.Number, - tournament_id: RecsType.Number, - }, - { - metadata: { - namespace: "zkube", - name: "Game", - types: [ - "u32", - "bool", - "u32", - "u32", - "u32", - "u8", - "u8", - "u8", - "u8", - "u8", - "u8", - "u8", - "u8", - "felt252", - "felt252", - "felt252", - "u8", - "u64", - "u32", - "u8", - "u8", - "u64", - ], - customTypes: [], - }, - }, - ), - Player: defineComponent( - world, - { - id: RecsType.BigInt, - game_id: RecsType.Number, - name: RecsType.BigInt, - points: RecsType.Number, - daily_streak: RecsType.Number, - last_active_day: RecsType.Number, - account_creation_day: RecsType.Number, - }, - { - metadata: { - namespace: "zkube", - name: "Player", - types: ["felt252", "u32", "felt252", "u32", "u8", "u32", "u32"], - customTypes: [], - }, - }, - ), - Credits: defineComponent( - world, - { - id: RecsType.BigInt, // player_id (address) - day_id: RecsType.Number, - remaining: RecsType.Number, - }, - { - metadata: { - namespace: "zkube", - name: "Credits", - types: ["felt252", "u64", "u8"], - customTypes: [], - }, - }, - ), - Settings: defineComponent( - world, - { - id: RecsType.Number, - free_daily_credits: RecsType.Number, - daily_mode_price: RecsType.BigInt, - normal_mode_price: RecsType.BigInt, - }, - { - metadata: { - namespace: "zkube", - name: "Settings", - types: ["u8", "u8", "felt252", "felt252"], - customTypes: [], - }, - }, - ), - Tournament: defineComponent( - world, - { - id: RecsType.Number, - is_set: RecsType.Boolean, - prize: RecsType.BigInt, - top1_player_id: RecsType.BigInt, - top2_player_id: RecsType.BigInt, - top3_player_id: RecsType.BigInt, - top1_score: RecsType.Number, - top2_score: RecsType.Number, - top3_score: RecsType.Number, - top1_claimed: RecsType.Boolean, - top2_claimed: RecsType.Boolean, - top3_claimed: RecsType.Boolean, - top1_game_id: RecsType.Number, - top2_game_id: RecsType.Number, - top3_game_id: RecsType.Number, - }, - { - metadata: { - namespace: "zkube", - name: "Tournament", - types: [ - "u64", - "bool", - "felt252", - "felt252", - "felt252", - "felt252", - "u32", - "u32", - "u32", - "bool", - "bool", - "bool", - "u32", - "u32", - "u32", - ], - customTypes: [], - }, - }, - ), - Chest: defineComponent( - world, - { - id: RecsType.Number, - point_target: RecsType.Number, - points: RecsType.Number, - prize: RecsType.BigInt, - }, - { - metadata: { - namespace: "zkube", - name: "Chest", - types: ["u32", "u32", "u32", "felt252"], - customTypes: [], - }, - }, - ), - Participation: defineComponent( - world, - { - chest_id: RecsType.Number, - player_id: RecsType.BigInt, - is_set: RecsType.Boolean, - points: RecsType.Number, - claimed: RecsType.Boolean, - }, - { - metadata: { - namespace: "zkube", - name: "Participation", - types: ["u32", "felt252", "bool", "u32", "bool"], - customTypes: [], - }, - }, - ), - Admin: defineComponent( - world, - { - id: RecsType.BigInt, - is_set: RecsType.Boolean, - }, - { - metadata: { - namespace: "zkube", - name: "Admin", - types: ["felt252", "bool"], - customTypes: [], - }, - }, - ), + Game: (() => { + return defineComponent( + world, + { + id: RecsType.Number, + over: RecsType.Boolean, + score: RecsType.Number, + moves: RecsType.Number, + next_row: RecsType.Number, + hammer_bonus: RecsType.Number, + wave_bonus: RecsType.Number, + totem_bonus: RecsType.Number, + hammer_used: RecsType.Number, + wave_used: RecsType.Number, + totem_used: RecsType.Number, + combo_counter: RecsType.Number, + max_combo: RecsType.Number, + blocks: RecsType.BigInt, + player_id: RecsType.BigInt, + seed: RecsType.BigInt, + mode: RecsType.Number, + start_time: RecsType.Number, + score_in_tournament: RecsType.Number, + combo_counter_in_tournament: RecsType.Number, + max_combo_in_tournament: RecsType.Number, + tournament_id: RecsType.Number, + }, + { + metadata: { + namespace: "zkube", + name: "Game", + types: [ + "u32", + "bool", + "u32", + "u32", + "u32", + "u8", + "u8", + "u8", + "u8", + "u8", + "u8", + "u8", + "u8", + "felt252", + "felt252", + "felt252", + "u8", + "u64", + "u32", + "u8", + "u8", + "u64", + ], + customTypes: [], + }, + }, + ); + })(), + Player: (() => { + return defineComponent( + world, + { + id: RecsType.BigInt, + game_id: RecsType.Number, + name: RecsType.BigInt, + points: RecsType.Number, + daily_streak: RecsType.Number, + last_active_day: RecsType.Number, + account_creation_day: RecsType.Number, + }, + { + metadata: { + namespace: "zkube", + name: "Player", + types: ["felt252", "u32", "felt252", "u32", "u8", "u32", "u32"], + customTypes: [], + }, + }, + ); + })(), + Credits: (() => { + return defineComponent( + world, + { + id: RecsType.BigInt, // player_id (address) + day_id: RecsType.Number, + remaining: RecsType.Number, + }, + { + metadata: { + namespace: "zkube", + name: "Credits", + types: ["felt252", "u64", "u8"], + customTypes: [], + }, + }, + ); + })(), + Settings: (() => { + return defineComponent( + world, + { + id: RecsType.Number, + free_daily_credits: RecsType.Number, + daily_mode_price: RecsType.BigInt, + normal_mode_price: RecsType.BigInt, + }, + { + metadata: { + namespace: "zkube", + name: "Settings", + types: ["u8", "u8", "felt252", "felt252"], + customTypes: [], + }, + }, + ); + })(), + Tournament: (() => { + return defineComponent( + world, + { + id: RecsType.Number, + is_set: RecsType.Boolean, + prize: RecsType.BigInt, + top1_player_id: RecsType.BigInt, + top2_player_id: RecsType.BigInt, + top3_player_id: RecsType.BigInt, + top1_score: RecsType.Number, + top2_score: RecsType.Number, + top3_score: RecsType.Number, + top1_claimed: RecsType.Boolean, + top2_claimed: RecsType.Boolean, + top3_claimed: RecsType.Boolean, + top1_game_id: RecsType.Number, + top2_game_id: RecsType.Number, + top3_game_id: RecsType.Number, + }, + { + metadata: { + namespace: "zkube", + name: "Tournament", + types: [ + "u64", + "bool", + "felt252", + "felt252", + "felt252", + "felt252", + "u32", + "u32", + "u32", + "bool", + "bool", + "bool", + "u32", + "u32", + "u32", + ], + customTypes: [], + }, + }, + ); + })(), + Chest: (() => { + return defineComponent( + world, + { + id: RecsType.Number, + point_target: RecsType.Number, + points: RecsType.Number, + prize: RecsType.BigInt, + }, + { + metadata: { + namespace: "zkube", + name: "Chest", + types: ["u32", "u32", "u32", "felt252"], + customTypes: [], + }, + }, + ); + })(), + Participation: (() => { + return defineComponent( + world, + { + chest_id: RecsType.Number, + player_id: RecsType.BigInt, + is_set: RecsType.Boolean, + points: RecsType.Number, + claimed: RecsType.Boolean, + }, + { + metadata: { + namespace: "zkube", + name: "Participation", + types: ["u32", "felt252", "bool", "u32", "bool"], + customTypes: [], + }, + }, + ); + })(), + Admin: (() => { + return defineComponent( + world, + { + id: RecsType.BigInt, + is_set: RecsType.Boolean, + }, + { + metadata: { + namespace: "zkube", + name: "Admin", + types: ["felt252", "bool"], + customTypes: [], + }, + }, + ); + })(), }; } diff --git a/client/src/dojo/setup.ts b/client/src/dojo/setup.ts index bf9ef46a..d6d05374 100644 --- a/client/src/dojo/setup.ts +++ b/client/src/dojo/setup.ts @@ -29,7 +29,7 @@ export async function setup({ ...config }: Config) { // await getSyncEntities(toriiClient, contractModels as any, []); const sync = await getSyncEntities( toriiClient, - Object.values(contractComponents), + contractComponents as any, [], 1000, ); diff --git a/client/src/hooks/useChest.tsx b/client/src/hooks/useChest.tsx index 626c8a49..1c5f941a 100644 --- a/client/src/hooks/useChest.tsx +++ b/client/src/hooks/useChest.tsx @@ -3,6 +3,7 @@ import { useMemo } from "react"; import { getEntityIdFromKeys } from "@dojoengine/utils"; import { useComponentValue } from "@dojoengine/react"; import { Entity } from "@dojoengine/recs"; +import useDeepMemo from "./useDeepMemo"; export const useChest = ({ id }: { id: number }) => { const { @@ -17,7 +18,7 @@ export const useChest = ({ id }: { id: number }) => { const key = useMemo(() => getEntityIdFromKeys([BigInt(id)]) as Entity, [id]); const component = useComponentValue(Chest, key); - const chest = useMemo(() => { + const chest = useDeepMemo(() => { return component ? new ChestClass(component) : null; }, [component]); diff --git a/client/src/hooks/useCredits.tsx b/client/src/hooks/useCredits.tsx index 7de85307..699678ab 100644 --- a/client/src/hooks/useCredits.tsx +++ b/client/src/hooks/useCredits.tsx @@ -3,6 +3,7 @@ import { useMemo } from "react"; import { getEntityIdFromKeys } from "@dojoengine/utils"; import { useComponentValue } from "@dojoengine/react"; import { Entity } from "@dojoengine/recs"; +import useDeepMemo from "./useDeepMemo"; export const useCredits = ({ playerId }: { playerId: string | undefined }) => { const { @@ -21,7 +22,7 @@ export const useCredits = ({ playerId }: { playerId: string | undefined }) => { //console.log("[useCredits] creditsKey", creditsKey); const component = useComponentValue(Credits, creditsKey); //console.log("[useCredits] component", component); - const credits = useMemo(() => { + const credits = useDeepMemo(() => { return component ? new CreditsClass(component) : null; }, [component]); //console.log("[useCredits] credits", credits); diff --git a/client/src/hooks/useDeepMemo.tsx b/client/src/hooks/useDeepMemo.tsx new file mode 100644 index 00000000..2c003f47 --- /dev/null +++ b/client/src/hooks/useDeepMemo.tsx @@ -0,0 +1,58 @@ +import { useRef } from "react"; + +export function deepCompare(a: any, b: any): boolean { + // Check for strict equality (handles primitives and reference equality) + if (a === b) return true; + + // Check for null or undefined + if (a == null || b == null) return a === b; + + // Check for Date objects + if (a instanceof Date && b instanceof Date) { + return a.getTime() === b.getTime(); + } + + // Check for Array objects + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) { + if (!deepCompare(a[i], b[i])) return false; + } + return true; + } + + // Check for plain objects + if (typeof a === "object" && typeof b === "object") { + // Compare object prototypes + if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false; + + const keysA = Object.keys(a); + const keysB = Object.keys(b); + + // Compare number of keys + if (keysA.length !== keysB.length) return false; + + // Compare keys in 'a' to keys in 'b' + for (const key of keysA) { + if (!Object.prototype.hasOwnProperty.call(b, key)) return false; + if (!deepCompare(a[key], b[key])) return false; + } + + return true; + } + + // If none of the above, values are not equal + return false; +} + +function useDeepMemo(factory: () => T, deps: any[]): T { + const valueRef = useRef<{ deps: any[]; value: T }>(); + + if (!valueRef.current || !deepCompare(valueRef.current.deps, deps)) { + valueRef.current = { deps, value: factory() }; + } + + return valueRef.current.value; +} + +export default useDeepMemo; diff --git a/client/src/hooks/useGame.tsx b/client/src/hooks/useGame.tsx index cdd5f805..56a91d29 100644 --- a/client/src/hooks/useGame.tsx +++ b/client/src/hooks/useGame.tsx @@ -3,6 +3,7 @@ import { useMemo } from "react"; import { getEntityIdFromKeys } from "@dojoengine/utils"; import { useComponentValue } from "@dojoengine/react"; import { Entity } from "@dojoengine/recs"; +import useDeepMemo from "./useDeepMemo"; export const useGame = ({ gameId, @@ -25,7 +26,7 @@ export const useGame = ({ ); const component = useComponentValue(Game, gameKey); - const game = useMemo(() => { + const game = useDeepMemo(() => { return component ? new GameClass(component) : null; }, [component]); diff --git a/client/src/hooks/useGrid.tsx b/client/src/hooks/useGrid.tsx index 5d69107d..44e7edd1 100644 --- a/client/src/hooks/useGrid.tsx +++ b/client/src/hooks/useGrid.tsx @@ -29,9 +29,13 @@ export const useGrid = ({ // Utiliser useEffect pour gérer le log quand la grille change useEffect(() => { + console.log("[useGrid] useEffect"); if (game?.blocks) { + console.log("[useGrid] game?.blocks", game?.blocks); + console.log("[useGrid] prevBlocksRef.current", prevBlocksRef.current); // Vérifier si la grille a changé if (!deepCompareNumberArrays(game.blocks, prevBlocksRef.current)) { + console.log("[useGrid] deepCompareNumberArrays"); // Si shouldLog est true, on log les données if (shouldLog) { const num = game.blocksRaw; diff --git a/client/src/hooks/usePlayer.tsx b/client/src/hooks/usePlayer.tsx index d858851b..2995fbbe 100644 --- a/client/src/hooks/usePlayer.tsx +++ b/client/src/hooks/usePlayer.tsx @@ -3,6 +3,7 @@ import { useMemo } from "react"; import { getEntityIdFromKeys } from "@dojoengine/utils"; import { useComponentValue } from "@dojoengine/react"; import { Entity } from "@dojoengine/recs"; +import useDeepMemo from "./useDeepMemo"; export const usePlayer = ({ playerId }: { playerId: string | undefined }) => { const { @@ -21,7 +22,7 @@ export const usePlayer = ({ playerId }: { playerId: string | undefined }) => { //console.log("playerKey", playerKey); const component = useComponentValue(Player, playerKey); //console.log("component", component); - const player = useMemo(() => { + const player = useDeepMemo(() => { return component ? new PlayerClass(component) : null; }, [component]); diff --git a/client/src/hooks/useSettings.tsx b/client/src/hooks/useSettings.tsx index 5dfd3ead..3873838b 100644 --- a/client/src/hooks/useSettings.tsx +++ b/client/src/hooks/useSettings.tsx @@ -3,6 +3,7 @@ import { useMemo } from "react"; import { getEntityIdFromKeys } from "@dojoengine/utils"; import { useComponentValue } from "@dojoengine/react"; import { Entity } from "@dojoengine/recs"; +import useDeepMemo from "./useDeepMemo"; export const useSettings = () => { const { @@ -20,7 +21,7 @@ export const useSettings = () => { ); const component = useComponentValue(Settings, settingsKey); - const settings = useMemo(() => { + const settings = useDeepMemo(() => { return component ? new SettingsClass(component) : null; }, [component]); diff --git a/client/src/hooks/useTournament.tsx b/client/src/hooks/useTournament.tsx index 97550e46..29d81fb1 100644 --- a/client/src/hooks/useTournament.tsx +++ b/client/src/hooks/useTournament.tsx @@ -5,6 +5,7 @@ import { Entity } from "@dojoengine/recs"; import { getEntityIdFromKeys } from "@dojoengine/utils"; import { useDojo } from "@/dojo/useDojo"; import { Tournament } from "@/dojo/game/models/tournament"; +import useDeepMemo from "./useDeepMemo"; interface TournamentInfo { id: number; @@ -58,6 +59,7 @@ const useTournament = (mode: ModeType): TournamentInfo => { ); const component = useComponentValue(Tournament, tournamentKey); + const tournament = useMemo(() => { return component ? new TournamentClass(component) : null; }, [component]); diff --git a/client/src/main.tsx b/client/src/main.tsx index 903f6edd..3624f441 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -28,10 +28,7 @@ export function Main() { const connectors = [cartridgeConnector]; const [setupResult, setSetupResult] = useState(null); - const loading = useMemo( - () => !setupResult, - [setupResult], - ); + const loading = useMemo(() => !setupResult, [setupResult]); useEffect(() => { async function initialize() { diff --git a/client/src/ui/screens/Home.tsx b/client/src/ui/screens/Home.tsx index 18b275dc..beb76611 100644 --- a/client/src/ui/screens/Home.tsx +++ b/client/src/ui/screens/Home.tsx @@ -49,7 +49,7 @@ export const Home = () => { useViewport(); useRewardsCalculator(); - useQuerySync(toriiClient, Object.values(contractComponents), []); + useQuerySync(toriiClient, contractComponents as any, []); const isSigning = false; //useAutoSignup(); @@ -145,9 +145,9 @@ export const Home = () => { prevGameOverRef.current = game?.over; }, [game?.over]); - useEffect(() => { + /*useEffect(() => { console.log("==================> Grid is changing"); - }, [grid]); + }, [grid]);*/ // Define render functions const renderDesktopView = () => ( @@ -328,7 +328,7 @@ export const Home = () => { // Check if game is over because otherwise we can display // previous game data on the board while the new game is starting // and torii indexing - initialGrid={game.isOver() ? [] : grid} + initialGrid={game.isOver() ? [] : game.blocks} nextLine={game.isOver() ? [] : game.next_row} score={game.isOver() ? 0 : game.score} combo={game.isOver() ? 0 : game.combo}