Skip to content

fix(badges): sort a copy in BadgesRow instead of mutating the prop#2435

Open
innolope-dev wants to merge 1 commit into
mainfrom
fix/badges-row-impure-sort
Open

fix(badges): sort a copy in BadgesRow instead of mutating the prop#2435
innolope-dev wants to merge 1 commit into
mainfrom
fix/badges-row-impure-sort

Conversation

@innolope-dev

@innolope-dev innolope-dev commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

What

BadgesRow's sortedBadges useMemo called badges.sort(...). Array.prototype.sort sorts in place, so the memo mutated the caller-owned badges array during render and returned the same reference. Sort a copy instead.

-return badges.sort((a, b) => {
+return [...badges].sort((a, b) => {

Severity — deliberately not oversold

There is most likely no user-visible symptom today. The only caller is PublicProfile.tsx, which passes profileBadges — a local useState array populated fresh from usersApi.getByUsername(...) on mount. The mutation hits a locally-owned array that no other consumer reads, and the resulting order is the desired one anyway.

It's worth fixing as correctness/robustness, not as a user-facing bug fix:

  1. A useMemo callback must be pure. Mutating during render is what React's concurrent rendering rules forbid.
  2. It's a live trap: the day someone passes a parent- or context-owned array (e.g. user.badges from the auth context), this silently reorders shared state.

Test

Added src/components/Badges/__tests__/BadgesRow.test.tsx, asserting the input array is not mutated. It fails before the fix (the caller's array comes back reordered to NEWEST, MIDDLE, OLDEST) and passes after — not a regression test that would pass either way.

Audit of other in-place sorts

Swept src for .sort( on arrays the caller doesn't own. BadgesRow was the only real offender. Everything else either already copies via [...arr] or sorts a fresh array returned from a .map/.filter chain:

Site Verdict
lib/blog.ts:53, lib/content.ts:298, badgeCelebration.utils.ts:101, dev/leaderboard/page.tsx:53, cardUnlock.types.ts:69 Sorts a fresh array from a map/filter chain, or a locally-built array — safe
HomeHistory.tsx:260, history/page.tsx:202 entries.sort(...) looks in-place, but entries is built via [...spread] locally (:158 / :166) — safe
useGeoFilteredPaymentOptions, CountryList, InvitesGraph, TokenSelector, shareAssetLayout, useContributePotFlow Already [...arr].sort(...)

Left them as-is rather than churn the diff.

Verification

  • pnpm typecheck — clean
  • npx jest src/components/Badges — 27 passed, 5 suites
  • npx eslint src/components/Badges — no new errors (the one error, badge.types.ts:11 no-explicit-any, is pre-existing; the new test's <img> warning matches the identical existing mock in BadgeEarnToast.test.tsx)
  • prettier --write on both touched files

Found incidentally during localization work on feat/app-localization; unrelated to i18n, so branched off main and kept separate.

Summary by CodeRabbit

  • Bug Fixes

    • Preserved the original badge order when displaying badges, preventing unintended changes to badge data.
  • Tests

    • Added coverage to verify that rendering the badges list does not alter the provided badge collection.

Array.prototype.sort is in-place, so the sortedBadges useMemo mutated the
caller-owned badges array during render. Sort a copy so the memo callback is
pure and safe under concurrent/StrictMode double-invocation.

No user-visible change today: the only caller passes locally-owned state that
is repopulated per mount, and the resulting order is the desired one anyway.
@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview, Comment Jul 16, 2026 7:22pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 06c74165-85d1-4b36-8cfb-c9e864ee8246

📥 Commits

Reviewing files that changed from the base of the PR and between 5ab943f and 89a23c6.

📒 Files selected for processing (2)
  • src/components/Badges/BadgesRow.tsx
  • src/components/Badges/__tests__/BadgesRow.test.tsx

📝 Walkthrough

Walkthrough

BadgesRow now sorts a shallow copy of the badges prop, preserving the caller’s array order. A unit test verifies that rendering does not mutate the input badges.

Changes

Badge rendering

Layer / File(s) Summary
Non-mutating badge sorting
src/components/Badges/BadgesRow.tsx, src/components/Badges/__tests__/BadgesRow.test.tsx
Badge sorting operates on a shallow copy, with a regression test confirming the original badge order remains unchanged.

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

Suggested labels: enhancement

🚥 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 and concisely describes the main change: sorting a copied badges array in BadgesRow to avoid mutating the prop.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/badges-row-impure-sort

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: dependency version conflict. Check your lock file or package.json.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 6206.68 → 6206.82 (+0.14)
Findings: 0 net (+1 new, -1 resolved)

🆕 New findings (1)

  • medium complexity — src/components/Badges/BadgesRow.tsx — CC 22, MI 66.49, SLOC 79

✅ Resolved (1)

  • src/components/Badges/BadgesRow.tsx — CC 22, MI 66.61, SLOC 79

@github-actions

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 1993 ran, 0 failed, 0 skipped, 33.9s

📊 Coverage (unit)

metric %
statements 59.7%
branches 43.3%
functions 48.6%
lines 60.0%
⏱ 10 slowest test cases
time test
3.5s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.3s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.5s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.4s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › keeps stickers off the username pill (final pass respects the keep-out)
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › Bank withdrawal keeps the $1 minimum for sub-$1 amounts
0.3s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in validateInviteCode
0.3s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in validateInviteCode body
0.2s src/utils/__tests__/demo-balance.test.ts › resetDemoBalance refills and restarts the TTL window
0.1s src/utils/__tests__/url.utils.test.ts › uses the public BASE_URL in Capacitor, not the localhost WebView origin
0.1s src/utils/__tests__/demo-balance.test.ts › auto-refills a wallet older than the TTL on cold start
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@kushagrasarathe kushagrasarathe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved. Correct fix for the impure useMemo (in-place Array.sort mutating a prop during render). Copy-then-sort is exactly right; in-place-sort audit spot-checked and clean. Regression test verified meaningful (fails pre-fix).

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.

2 participants