cache shuffled game order in zustand store to prevent card flicker

the discover tab re-shuffled on every mount because useGames() returns
a new array reference each time. now the shuffled ID order is stored in
zustand, only recomputed when game count or seen count actually changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 23:59:30 +01:00
parent 2c8141660c
commit 2d50198782
2 changed files with 43 additions and 10 deletions

View File

@@ -1,14 +1,14 @@
import { useGames, usePlaylist, usePlaylistMutations } from "@/shared/db/hooks"
import type { Game } from "@/shared/db/schema"
import { useCallback, useEffect, useMemo, useState } from "react"
import { seededShuffle, useDiscoverStore } from "../store"
import { useDiscoverStore } from "../store"
export function useDiscover() {
const { games: allGames } = useGames()
const { games: wantToPlayGames, reload: reloadWtp } = usePlaylist("want-to-play")
const { games: notIntGames, reload: reloadNi } = usePlaylist("not-interesting")
const { addGame } = usePlaylistMutations()
const { currentIndex, setCurrentIndex, seed, reset } = useDiscoverStore()
const { currentIndex, setCurrentIndex, updateShuffledGames, reset } = useDiscoverStore()
const [ready, setReady] = useState(false)
const [localSeenIds, setLocalSeenIds] = useState<Set<string>>(new Set())
@@ -21,12 +21,8 @@ export function useDiscover() {
}, [wantToPlayGames, notIntGames, localSeenIds])
const unseenGames = useMemo(
() =>
seededShuffle(
allGames.filter((g) => !seenIds.has(g.id)),
seed,
),
[allGames, seenIds, seed],
() => updateShuffledGames(allGames, seenIds),
[allGames, seenIds, updateShuffledGames],
)
useEffect(() => {

View File

@@ -1,21 +1,58 @@
import type { Game } from "@/shared/db/schema"
import { create } from "zustand"
interface DiscoverState {
currentIndex: number
seed: number
animatingDirection: "left" | "right" | null
/** Cached shuffled game order (game IDs) */
shuffledIds: string[]
/** Fingerprint of the input used to produce shuffledIds */
shuffleKey: string
setCurrentIndex: (index: number) => void
setAnimatingDirection: (dir: "left" | "right" | null) => void
/** Update the shuffled order only when the underlying game list changes */
updateShuffledGames: (games: Game[], seenIds: Set<string>) => Game[]
reset: () => void
}
export const useDiscoverStore = create<DiscoverState>((set) => ({
function buildShuffleKey(games: Game[], seenIds: Set<string>, seed: number): string {
return `${games.length}:${seenIds.size}:${seed}`
}
export const useDiscoverStore = create<DiscoverState>((set, get) => ({
currentIndex: 0,
seed: Math.random(),
animatingDirection: null,
shuffledIds: [],
shuffleKey: "",
setCurrentIndex: (currentIndex) => set({ currentIndex }),
setAnimatingDirection: (animatingDirection) => set({ animatingDirection }),
reset: () => set({ currentIndex: 0, seed: Math.random(), animatingDirection: null }),
updateShuffledGames: (games, seenIds) => {
const { seed, shuffleKey, shuffledIds } = get()
const key = buildShuffleKey(games, seenIds, seed)
if (key === shuffleKey && shuffledIds.length > 0) {
// Reuse cached order — just resolve IDs back to game objects
const byId = new Map(games.map((g) => [g.id, g]))
return shuffledIds.flatMap((id) => {
if (seenIds.has(id)) return []
const g = byId.get(id)
return g ? [g] : []
})
}
const unseen = games.filter((g) => !seenIds.has(g.id))
const shuffled = seededShuffle(unseen, seed)
set({ shuffledIds: shuffled.map((g) => g.id), shuffleKey: key })
return shuffled
},
reset: () =>
set({
currentIndex: 0,
seed: Math.random(),
animatingDirection: null,
shuffledIds: [],
shuffleKey: "",
}),
}))
/** Mulberry32 — fast seeded 32-bit PRNG */