<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Uduak Ukpong</title>
    <description>The latest articles on DEV Community by Uduak Ukpong (@udlxix).</description>
    <link>https://dev.to/udlxix</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4119663%2F7d162ce3-4f3d-4d60-a03f-94c0d545f763.jpeg</url>
      <title>DEV Community: Uduak Ukpong</title>
      <link>https://dev.to/udlxix</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/udlxix"/>
    <language>en</language>
    <item>
      <title>Stop Using a mounted Flag to Fix Theme Flash: Use useSyncExternalStore Instead</title>
      <dc:creator>Uduak Ukpong</dc:creator>
      <pubDate>Thu, 10 Sep 2026 17:42:22 +0000</pubDate>
      <link>https://dev.to/udlxix/stop-using-a-mounted-flag-to-fix-theme-flash-use-usesyncexternalstore-instead-4emo</link>
      <guid>https://dev.to/udlxix/stop-using-a-mounted-flag-to-fix-theme-flash-use-usesyncexternalstore-instead-4emo</guid>
      <description>&lt;p&gt;The login page went blank in production. Not a broken layout, not a missing stylesheet. Blank: the whole subtree that the sidebar belonged to just disappeared from the DOM right after the page finished loading. It only happened on a real production build (&lt;code&gt;next build &amp;amp;&amp;amp; next start&lt;/code&gt;), never in &lt;code&gt;next dev&lt;/code&gt;, and only when the stored theme preference was &lt;code&gt;"dark"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The cause was a theme toggle.&lt;/p&gt;

&lt;p&gt;If you've worked with the Next.js App Router, you already know the usual shape of a dark-mode toggle: some client state holding &lt;code&gt;"light"&lt;/code&gt; or &lt;code&gt;"dark"&lt;/code&gt;, an effect that writes it onto the DOM, a button that flips it. You've probably also met the classic symptom that comes with getting this wrong: a flash of the wrong theme for a frame or two on load. What you might not know, since it's a narrower topic, is Trusted Types: a browser security feature that helps prevent DOM-based XSS by blocking untrusted strings from being written into dangerous sinks like &lt;code&gt;innerHTML&lt;/code&gt;, unless an approved policy sanctions them. If a Trusted Types policy is enforced and something tries to write raw HTML through a path the policy didn't approve, the browser throws instead of rendering it. Keep that in your pocket. It's the reason this bug was a crash and not just a flicker.&lt;/p&gt;

&lt;p&gt;This is Atlas, a project-management app I built solo (real Postgres row-level security, an append-only activity log written entirely by database triggers, accessibility treated as a requirement rather than a feature). It's not running at scale with real traffic. It doesn't need to be. Not for this story. What it has is a CI pipeline, a test suite, and one bug I had to actually diagnose, not simulate for a tutorial.&lt;/p&gt;

&lt;p&gt;Here's what caused it, why the fix everyone reaches for first doesn't actually fix it, and what does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The disagreement under the flash
&lt;/h2&gt;

&lt;p&gt;"Flash of wrong theme" undersells what's happening. It's not really a timing problem. It's a disagreement. Next.js renders your app to HTML on the server, where there's no &lt;code&gt;window&lt;/code&gt;, no &lt;code&gt;localStorage&lt;/code&gt;, no way to know what a returning visitor picked last time. React then hydrates that HTML on the client, attaching event handlers and reconciling its idea of the tree against what's already in the DOM. If the client's first render produces different output than the server did, React has a mismatch on its hands.&lt;/p&gt;

&lt;p&gt;In Atlas, that mismatch traced back to one function: the lazy initializer inside &lt;code&gt;ThemeContext&lt;/code&gt;'s &lt;code&gt;useState&lt;/code&gt; call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setTheme&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Theme&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;atlas-theme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stored&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;stored&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Defer to system preference if no stored theme, and persist that choice.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;system&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;(prefers-color-scheme: dark)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;atlas-theme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;system&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;system&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that &lt;code&gt;typeof window&lt;/code&gt; check carefully, because the bug is hiding in what it doesn't guard. On the server, &lt;code&gt;window&lt;/code&gt; is genuinely undefined, so this returns &lt;code&gt;"light"&lt;/code&gt;, always. But on the client, this initializer doesn't just run once at some safe point after mount. It runs during React's very first client render, the hydration render, and by then &lt;code&gt;window&lt;/code&gt; already exists. So the client's first render skips the &lt;code&gt;"light"&lt;/code&gt; fallback entirely and reads &lt;code&gt;localStorage&lt;/code&gt; immediately, returning whatever the user actually had stored.&lt;/p&gt;

&lt;p&gt;Server HTML gets built from the forced &lt;code&gt;"light"&lt;/code&gt; branch. But on the client, that first render reads whatever was actually stored. If your stored theme was &lt;code&gt;"dark"&lt;/code&gt;, the two disagree on the very first paint, before React has done anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it was a crash, not a flicker
&lt;/h2&gt;

&lt;p&gt;Most of the time, a hydration mismatch is just visually annoying: React discards the mismatched subtree and regenerates it client-side, and you get a flash. In Atlas, &lt;code&gt;Sidebar.tsx&lt;/code&gt; was the one component whose &lt;em&gt;render output&lt;/em&gt; actually branched on &lt;code&gt;theme&lt;/code&gt; (the icon, the label, the button), so it was the one place this showed up at all.&lt;/p&gt;

&lt;p&gt;In React, when that client regeneration reaches an inline script, React recreates the script element by first setting a raw &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; string through a temporary div's &lt;code&gt;innerHTML&lt;/code&gt;. Atlas runs a Trusted Types policy through a script in &lt;code&gt;app/layout.tsx&lt;/code&gt; that only defines &lt;code&gt;createScriptURL&lt;/code&gt; (needed for the app's own chunk loader), not &lt;code&gt;createHTML&lt;/code&gt;. Recovery hit a browser API it wasn't authorized to use, and threw. In production, with the policy actually enforced, that meant recovery failed. The failure took the rest of the screen with it. It just stayed blank. I'm not going to get into the design of that Trusted Types policy here. That's a separate topic. What matters for this piece is the consequence: a hydration mismatch that would be a cosmetic flash almost anywhere else was a blank screen in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tempting fix, and where it actually falls short
&lt;/h2&gt;

&lt;p&gt;The standard answer to "my render depends on something the server can't know" is a &lt;code&gt;mounted&lt;/code&gt; flag. It's not from Atlas. It's just the shape most of us reach for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ThemeIcon&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setMounted&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setMounted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;mounted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;IconPlaceholder&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Moon&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Sun&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Give this a fair hearing, because it does solve the mismatch. Server and client both render the placeholder on the first pass, since &lt;code&gt;mounted&lt;/code&gt; starts &lt;code&gt;false&lt;/code&gt; in both places. No disagreement, no crash. The real icon only shows up once &lt;code&gt;useEffect&lt;/code&gt; fires after mount, which is client-only by definition.&lt;/p&gt;

&lt;p&gt;Here's where it stops being a fix and starts being a workaround. It always requires a post-mount render before showing the real value, even when there is no hydration mismatch to avoid. It trades a wrong-icon flash for a placeholder-then-pop-in flash (better, but still a flash). Also, it doesn't touch the actual problem: React still treats its own state as the source of truth. The flag is a manual "don't trust yourself yet" gate bolted onto the read side, and you have to remember to bolt it on again at every component that needs the theme. Extract that pattern into a shared hook, which you inevitably will once you have two or three call sites, and you're one small step from&amp;nbsp;useSyncExternalStore&amp;nbsp;anyway, just without the guarantee it actually gives you.&lt;/p&gt;

&lt;p&gt;The deeper issue: &lt;code&gt;theme&lt;/code&gt; was never really React's state to own in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the theme actually lives
&lt;/h2&gt;

&lt;p&gt;By the time React starts hydrating anything, the correct theme is already sitting on the DOM. Atlas sets it with an inline script in the root layout, written as a raw &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tag via &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt; (not &lt;code&gt;next/script&lt;/code&gt;, which would load too late to beat the paint):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(){&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;atlas-theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="o"&gt;===&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="o"&gt;===&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;);}&lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(prefers-color-scheme: dark)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-theme&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);}}&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;){}})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reads the stored preference, falls back to &lt;code&gt;matchMedia&lt;/code&gt; if there isn't one, and sets &lt;code&gt;data-theme&lt;/code&gt; on &lt;code&gt;document.documentElement&lt;/code&gt;. The whole thing is wrapped in a &lt;code&gt;try/catch&lt;/code&gt;, so a blocked &lt;code&gt;localStorage&lt;/code&gt; (private browsing, strict cookie settings) just skips silently instead of breaking the page. This runs before React hydrates anything. &lt;code&gt;data-theme&lt;/code&gt; is correct on the &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt; element from the first paint.&lt;/p&gt;

&lt;p&gt;React just doesn't know that. Nothing has told it. And that's the actual bug: not when the value becomes available, but which system owns it. The theme lives in the DOM. React needs to &lt;em&gt;read&lt;/em&gt; an external source, not maintain its own parallel copy of the same fact.&lt;/p&gt;

&lt;p&gt;That's exactly what &lt;code&gt;useSyncExternalStore&lt;/code&gt; is for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The implementation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useSyncExternalStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;DisplayedTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MutationObserver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;attributeFilter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data-theme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;observer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getSnapshot&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;DisplayedTheme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;documentElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data-theme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getServerSnapshot&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;DisplayedTheme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useDisplayedTheme&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;DisplayedTheme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;useSyncExternalStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getSnapshot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;getServerSnapshot&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three functions, each doing one job. &lt;code&gt;getSnapshot&lt;/code&gt; doesn't hold state. It reads &lt;code&gt;data-theme&lt;/code&gt; straight off &lt;code&gt;documentElement&lt;/code&gt;, which means it's always reporting what's actually there, not what React last remembered setting. &lt;code&gt;subscribe&lt;/code&gt; sets up a &lt;code&gt;MutationObserver&lt;/code&gt; scoped tightly with &lt;code&gt;attributeFilter: ["data-theme"]&lt;/code&gt;, so it only fires when that one attribute changes, not on every DOM mutation in the document. And &lt;code&gt;getServerSnapshot&lt;/code&gt; is where the interesting decision lives, but I'll get to that in a moment.&lt;/p&gt;

&lt;p&gt;Notice what &lt;code&gt;useDisplayedTheme&lt;/code&gt; doesn't do: it doesn't write anything. Atlas keeps write and read on two separate paths on purpose. &lt;code&gt;ThemeContext&lt;/code&gt; still owns the toggle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;toggleTheme&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;setTheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;atlas-theme&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clicking the button updates React's own &lt;code&gt;theme&lt;/code&gt; state and writes the choice to &lt;code&gt;localStorage&lt;/code&gt;, both inside &lt;code&gt;toggleTheme&lt;/code&gt; itself. A &lt;code&gt;useEffect&lt;/code&gt; elsewhere in &lt;code&gt;ThemeContext&lt;/code&gt; then reflects that updated state onto &lt;code&gt;data-theme&lt;/code&gt;. But &lt;code&gt;Sidebar.tsx&lt;/code&gt;, the component that actually renders based on the theme, doesn't read &lt;code&gt;theme&lt;/code&gt; from that context at all anymore. It reads &lt;code&gt;useDisplayedTheme()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;toggleTheme&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useTheme&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;displayedTheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useDisplayedTheme&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isThemePending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;displayedTheme&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Write goes through the context's state, because a click handler needs somewhere to hold intent. Read goes through the DOM, because the DOM is what's actually true, including on that first render where React's own state hasn't caught up yet. Two different sources, chosen for two different jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest third state
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;getServerSnapshot&lt;/code&gt; returning &lt;code&gt;"pending"&lt;/code&gt; instead of guessing &lt;code&gt;"light"&lt;/code&gt; is the detail that makes the rest of this actually work, not just look tidy.&lt;/p&gt;

&lt;p&gt;During hydration, &lt;code&gt;useSyncExternalStore&lt;/code&gt; calls &lt;code&gt;getServerSnapshot&lt;/code&gt; and only &lt;code&gt;getServerSnapshot&lt;/code&gt;. It never touches &lt;code&gt;getSnapshot&lt;/code&gt; on that first pass. So the server renders &lt;code&gt;"pending"&lt;/code&gt;, and the client's first render also produces &lt;code&gt;"pending"&lt;/code&gt;, by construction, since both sides call the exact same function. There's no way for these two to disagree, because they're not two guesses that happen to agree. They're the same fixed answer, sourced from the same place, every time. No mismatch, no recovery path, no &lt;code&gt;innerHTML&lt;/code&gt; write for Trusted Types to reject.&lt;/p&gt;

&lt;p&gt;The real theme shows up afterward, once the observer's &lt;code&gt;getSnapshot&lt;/code&gt; takes over, and that's an ordinary re-render triggered by a normal state update, not an error being recovered from. &lt;code&gt;Sidebar.tsx&lt;/code&gt; uses &lt;code&gt;isThemePending&lt;/code&gt; to make that in-between moment honest instead of invisible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;
  &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;toggleTheme&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isThemePending&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;aria-busy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isThemePending&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;aria-label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;isThemePending&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Loading theme preference&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;displayedTheme&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Switch to dark mode&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Switch to light mode&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;mounted&lt;/code&gt; flag's placeholder state is a UI trick, something to look at while the real thing loads. &lt;code&gt;"pending"&lt;/code&gt; is a real value in the type (&lt;code&gt;DisplayedTheme = "light" | "dark" | "pending"&lt;/code&gt;), one the button's &lt;code&gt;disabled&lt;/code&gt; and &lt;code&gt;aria-busy&lt;/code&gt; attributes respond to directly. The type system knows there are three possible states here, not two, so nothing downstream can quietly pretend the answer is always known.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this costs, and when it isn't worth it
&lt;/h2&gt;

&lt;p&gt;There was a cleaner option on the table: read the theme from a cookie on the server and pass it down, which removes even the brief "pending" window entirely. It got rejected. &lt;code&gt;cookies()&lt;/code&gt; unconditionally opts a route into dynamic rendering, and several of Atlas's routes are static today. Giving that up wasn't worth it. It would mean restructuring the dashboard layout out of being a single client component, just to remove a sub-100ms loading state. That's a call that could reasonably go the other way in a different app. It's not a universal rule, just the one that fit here.&lt;/p&gt;

&lt;p&gt;More broadly, none of this is worth reaching for unless two things are both true: your app has state set on the DOM before React hydrates, and something's render output actually branches on that state. If your theme only drives CSS custom properties and nothing in your JSX conditionally renders based on it, there's no mismatch to have in the first place. &lt;code&gt;data-theme&lt;/code&gt; can just sit there and your styles read it, no hook required.&lt;/p&gt;

&lt;p&gt;The mainstream approach is to let the theme drive CSS (a class or data attribute on the html element that Tailwind or your stylesheet reads) and keep your JSX theme-agnostic. Most theme-dependent UI, including a toggle icon, is better handled that way, often by rendering both states and swapping them with CSS. I branched on the theme in JavaScript for one reason: the toggle's aria-label ("Switch to dark mode" versus "Switch to light mode") is an accessible name, and you cannot set an accessible name from a CSS class. Accessibility was a first-class requirement in Atlas, not a coat of paint, so the toggle had to announce its actual state to assistive tech. Once one thing in that component genuinely needed the theme as a JavaScript value, reading it correctly was the real problem, and that is what &lt;code&gt;useSyncExternalStore&lt;/code&gt; solves. &lt;code&gt;Sidebar.tsx&lt;/code&gt; needed this because it was the one place in Atlas where a component's actual output, not just its appearance, depended on a value React didn't have yet. That's the specific condition to look for, not "I have a theme toggle."&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>nextjs</category>
      <category>react</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
