Skip to content

Fix: High score calculation uses stale state (Issue #916) - #1436

Open
sahare77 wants to merge 1 commit into
Canopus-Labs:mainfrom
sahare77:fix/916-highscore-stale-state
Open

Fix: High score calculation uses stale state (Issue #916)#1436
sahare77 wants to merge 1 commit into
Canopus-Labs:mainfrom
sahare77:fix/916-highscore-stale-state

Conversation

@sahare77

@sahare77 sahare77 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary of What Has Been Done

In the Memory Match game component, the high score calculation was using the stale score state variable before React finished updating it asynchronously. This caused the game to sometimes save an incorrect, outdated high score upon completion.

Changes Made

  • Modified rontend/src/hooks/useMemoryMatch.js.
  • Combined the time/perfect bonuses and highScore update into a single setScore functional update.
  • Used prevScore to calculate the exact, up-to-date final score, passing it immediately to setHighScore.

Impact it Made

  • Ensures fair and accurate leaderboards and progress tracking.
  • Resolves user frustration regarding lost high scores upon game completion.

Closes #916

Summary

  • Fixed Memory Match high-score calculation.
  • Applied time and perfect-game bonuses in one functional setScore update.
  • Calculated the final score from prevScore.
  • Persisted the final score with setHighScore.
  • Kept achievement and perfect-game handling unchanged.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Victory scoring now calculates the final bonus-adjusted score in one functional update. High-score persistence uses that final score instead of a captured stale value. The time bonus multiplier changes from 15 to 10.

Changes

Memory Match scoring

Layer / File(s) Summary
Final victory score calculation
frontend/src/hooks/useMemoryMatch.js
The victory flow calculates the time bonus from the configured expected time and multiplier. It applies time and perfect-game bonuses together. High-score comparison and persistence use the calculated final score.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: sujalmahapatra

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the stale-state high-score calculation bug and references linked issue #916.
Linked Issues check ✅ Passed The changes calculate the final score from the functional update and use it for high-score persistence, satisfying issue #916.
Out of Scope Changes check ✅ Passed The changes are limited to Memory Match scoring and high-score persistence, which directly support issue #916.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/hooks/useMemoryMatch.js`:
- Around line 324-338: The setScore updater in useMemoryMatch must remain pure:
remove the nested setHighScore and localStorage.setItem side effects. Add a
post-commit useEffect that, when phase is "victory" and score exceeds highScore,
updates highScore and persists the new value to localStorage.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 024bc194-2d35-4bcc-8380-73c2ffe2a82b

📥 Commits

Reviewing files that changed from the base of the PR and between 8acb5b8 and 047e0d0.

📒 Files selected for processing (1)
  • frontend/src/hooks/useMemoryMatch.js

Comment on lines +324 to +338
// Apply the final bonuses to score and calculate high score accurately using functional update
setScore((prevScore) => {
let finalScore = prevScore;
if (timeElapsed < config.maxExpectedTime) finalScore += timeBonus;
if (isPerfect) finalScore += 500;

setHighScore((prevHigh) => {
if (finalScore > prevHigh) {
localStorage.setItem("memory_match_high_score", finalScore.toString());
return finalScore;
}
return prevHigh;
});

return finalScore;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In React 18.3.1, can a useState functional updater safely call another state setter or localStorage.setItem? Find official React documentation on updater purity and Strict Mode updater invocation.

💡 Result:

No, a useState functional updater cannot safely call localStorage.setItem or perform other side effects [1][2]. Official React documentation explicitly states that updater functions must be pure [1][2]. React expects updater functions to perform only one task: calculate and return the next state based on the current state [1][2]. They must not change any objects or variables, nor should they perform any side effects like writing to localStorage, as these actions violate the requirement for purity [3][4][5]. This requirement is enforced during development by React's Strict Mode [6]. In Strict Mode, React intentionally calls your updater function twice [1][6]. If your updater function performs a side effect such as localStorage.setItem, that side effect will be executed twice [6][7]. While this may not always cause a crash, it is considered incorrect behavior that can lead to bugs, inconsistent application state, or performance issues, especially because React reserves the right to call these functions multiple times during the rendering phase [1][8][5]. To safely trigger side effects like updating localStorage when state changes, you should use the useEffect hook [3][4]. Alternatively, if you need to coordinate state updates with other logic, you can perform the side effect in an event handler or use a wrapped state setter pattern where the side effect is triggered alongside the state update, rather than inside the updater function itself [3][9]. Sources: [1] https://react.dev/reference/react/useState [3] https://react.dev/learn/keeping-components-pure [8] reactjs/react.dev#5982 [4] https://react.dev/reference/rules [6] https://react.dev/reference/react/StrictMode [7] https://legacy.reactjs.org/docs/strict-mode.html [5] https://stackoverflow.com/questions/71780110/is-it-okey-to-use-side-effects-in-the-usestate-hook-callback [2] https://react.dev/learn/queueing-a-series-of-state-updates [9] https://kyleshevlin.com/wrapped-state-setter-pattern/

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

file="frontend/src/hooks/useMemoryMatch.js"
printf '%s\n' '--- target file outline ---'
ast-grep outline "$file" --lang javascript || true

printf '%s\n' '--- relevant state and effect references ---'
rg -n -C 4 'useState|useEffect|setScore|setHighScore|localStorage|phase|victory|finalScore' "$file"

Repository: Canopus-Labs/PrepPilot

Length of output: 6517


🏁 Script executed:

#!/bin/bash
set -eu

file="frontend/src/hooks/useMemoryMatch.js"
printf '%s\n' '--- game initialization and reset ---'
sed -n '130,220p' "$file"
printf '%s\n' '--- victory implementation ---'
sed -n '288,342p' "$file"
printf '%s\n' '--- hook return and remaining effects ---'
sed -n '342,410p' "$file"
printf '%s\n' '--- project React version and tests ---'
rg -n '"react"|"react-dom"|StrictMode|useMemoryMatch|memory_match_high_score' \
  package.json frontend/package.json frontend/src frontend/test* frontend/tests 2>/dev/null || true

Repository: Canopus-Labs/PrepPilot

Length of output: 20913


🌐 Web query:

site:react.dev/reference/react/useState updater function pure React may call updater twice Strict Mode side effects

💡 Result:

In React's Strict Mode, React intentionally calls your state updater functions twice during development [1][2]. This is a development-only behavior designed to help you identify accidental impurities in your code [1][3]. React uses the result of one of these calls and ignores the result of the other [1][4]. If your updater function is pure—meaning it does not produce side effects and consistently returns the same output for the same input—this double invocation will not affect your application's logic or behavior [1][5]. Because this mechanism is intended to enforce purity, if you notice issues when the function is called twice, it is a signal that your updater function may be performing side effects (such as modifying objects in place, performing network requests, or updating global variables), which should be moved to event handlers or useEffect hooks [1][2]. This behavior does not occur in production and does not affect event handlers, which do not need to be pure [1][6].

Citations:


Keep the setScore updater pure.

Move setHighScore and localStorage.setItem to a post-commit useEffect. React Strict Mode can invoke functional updaters twice, which can persist a high score before the update commits. Synchronize the high score when phase === "victory" and score > highScore.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/hooks/useMemoryMatch.js` around lines 324 - 338, The setScore
updater in useMemoryMatch must remain pure: remove the nested setHighScore and
localStorage.setItem side effects. Add a post-commit useEffect that, when phase
is "victory" and score exceeds highScore, updates highScore and persists the new
value to localStorage.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: High score calculation may use stale score state after game completion

1 participant