Skip to content

fix: read prefers-color-scheme via useSyncExternalStore (Category B, #200) - #367

Open
JohnRDOrazio wants to merge 1 commit into
fix/set-state-in-effect-category-afrom
fix/set-state-in-effect-category-b
Open

fix: read prefers-color-scheme via useSyncExternalStore (Category B, #200)#367
JohnRDOrazio wants to merge 1 commit into
fix/set-state-in-effect-category-afrom
fix/set-state-in-effect-category-b

Conversation

@JohnRDOrazio

Copy link
Copy Markdown
Member

Stacked on #365. Base is fix/set-state-in-effect-category-a, so the diff here is just the Category B change. Retarget to dev once #365 merges.

Category B of #200browser-API subscriptions.

The one site I changed

TurtleEditor.tsx:110 mirrored window.matchMedia("(prefers-color-scheme: dark)") into React state from an effect: seed "light", correct after the commit, then keep a manual change listener in sync. matchMedia is an external store, which is exactly what useSyncExternalStore exists for.

New lib/hooks/usePrefersDarkMode.ts subscribes to the media query directly. Three things get better:

  • The value is correct on the first committed render instead of light-first-then-corrected — no flash of the light Monaco theme for dark-mode users.
  • Concurrent readers can't tear.
  • It degrades to light when matchMedia is absent, which the old inline typeof window !== "undefined" guard only did implicitly.

getServerSnapshot returns false, which matches both the old initial value and the fact that the server can't know the visitor's OS preference — so no hydration mismatch and no behaviour change.

The other site is deliberately untouched

#200 lists lib/hooks/useCollaborationStatus.ts:132 as the second Category B site, suggesting the WebSocket lifecycle be wrapped in a small store.

That code is deleted by #154. Issue #140 is that the hook was pointing at the lint endpoint rather than a collaboration one, and #154's fix replaces the whole implementation with a stub:

export function useCollaborationStatus(_options: { projectId: string; enabled?: boolean }) {
  const status: ConnectionState = "disabled";
  return { status, isConnected: false, endpoint: "/api/v1/collab/ws", purpose: "Real-time collaboration (coming soon)" };
}

I checked: that version contains zero useState/useEffect, so the warning goes away with it, and #154 currently merges cleanly with dev. Building a useSyncExternalStore store here would mean writing a store for code that is about to be removed, and would conflict with #154 for no benefit. Both #140 and #200 are v0.4.0, so there's no milestone reason to prefer one order — but #154 does strictly more, so it should go first.

Category B therefore closes out as: this PR + #154.

Verification against the #365 base

  • react-hooks/set-state-in-effect: 10 → 9 warnings (and → 8 once refactor: decouple useCollaborationStatus from lint WebSocket endpoint #154 lands).
  • npm run type-check clean, npm run lint 0 errors, npm run build succeeds.
  • npx vitest --run — 160 files / 2759 tests pass. The 18 existing TurtleEditor tests pass unchanged.
  • 5 new tests for usePrefersDarkMode: correct value on first render, light default, live response to preference changes, unsubscribe on unmount, and the missing-matchMedia fallback.

Scope

Partial progress on #200 — does not close it. Rule stays at "warn".

…200)

TurtleEditor mirrored `window.matchMedia("(prefers-color-scheme: dark)")`
into React state from an effect: it seeded "light", then corrected after
the commit, then kept a manual change listener in sync. matchMedia is an
external store, so it belongs in useSyncExternalStore.

Adds `usePrefersDarkMode`, which subscribes to the media query directly.
The value is already correct on the first committed render instead of
light-first-then-corrected, and concurrent readers cannot tear.
`getServerSnapshot` returns false, matching both the old initial value
and the fact that the server cannot know the visitor's OS preference, so
there is no hydration mismatch and no behaviour change.

The hook also degrades to light when `matchMedia` is missing, which the
previous inline `typeof window !== "undefined"` guard did implicitly.

`react-hooks/set-state-in-effect`: 10 warnings → 9.

Category B's other site, `useCollaborationStatus.ts:132`, is deliberately
untouched — see the PR description.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 316fa7f1-4841-4d11-b070-96cc280a00c6

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@codecov

codecov Bot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.71429% with 2 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
components/editor/TurtleEditor.tsx 50.00% 0 Missing and 1 partial ⚠️
lib/hooks/usePrefersDarkMode.ts 91.66% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

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.

1 participant