<?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: Izak T</title>
    <description>The latest articles on DEV Community by Izak T (@kickbuttowski80).</description>
    <link>https://dev.to/kickbuttowski80</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%2F98383%2Ff6294d6b-373a-4e86-8694-ca306979a3eb.jpeg</url>
      <title>DEV Community: Izak T</title>
      <link>https://dev.to/kickbuttowski80</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kickbuttowski80"/>
    <language>en</language>
    <item>
      <title>Perry Mason in: The Case of the Drifting Timer</title>
      <dc:creator>Izak T</dc:creator>
      <pubDate>Thu, 20 Aug 2026 00:47:41 +0000</pubDate>
      <link>https://dev.to/kickbuttowski80/perry-mason-in-the-case-of-the-drifting-timer-inj</link>
      <guid>https://dev.to/kickbuttowski80/perry-mason-in-the-case-of-the-drifting-timer-inj</guid>
      <description>&lt;h1&gt;
  
  
  Perry Mason in: The Case of the Drifting Timer
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Opening Statement
&lt;/h2&gt;

&lt;p&gt;You need a reactive "current time" in your Vue 3 app. A schedule grid with a red line showing "now." A live clock. A dashboard that updates every minute.&lt;/p&gt;

&lt;p&gt;Every Vue developer reaches for &lt;code&gt;setInterval&lt;/code&gt; first. It works. But "works" and "works well" are different things.&lt;/p&gt;

&lt;p&gt;This is the story of taking a naive timer from "it ticks" to production-grade — and the four iterations it took to get there. The prosecution calls four exhibits. Let's begin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exhibit A: The Memory Leak
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="nf"&gt;onMounted&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;setInterval&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="nx"&gt;currentTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&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;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;60000&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;It works. Sort of. The defense rests — but the prosecution is just getting started.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exhibits of negligence:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The interval is never cleared. When the component unmounts, the timer keeps firing every 60 seconds forever — updating a ref nothing reads anymore, and holding its closure (and everything the ref references) in memory for the lifetime of the page.&lt;/li&gt;
&lt;li&gt;Silent. Invisible. The kind of leak that shows up in production after a user navigates around your app for 20 minutes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Exhibit B: Component-Only Cleanup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;timeInterval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;

&lt;span class="nf"&gt;onMounted&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="nx"&gt;currentTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&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;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;timeInterval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&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="nx"&gt;currentTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&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;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;60000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nf"&gt;onUnmounted&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="nx"&gt;timeInterval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeInterval&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;Now we clean up. The interval is stored in a variable, cleared on unmount. A step forward. But &lt;code&gt;onUnmounted&lt;/code&gt; has a scope limitation worth understanding:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The limitation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;onUnmounted&lt;/code&gt; only works inside components. If someone calls this logic from a Pinia store or outside a component's &lt;code&gt;setup()&lt;/code&gt; context, &lt;code&gt;onUnmounted&lt;/code&gt; never fires. The timer leaks silently. (Composables called &lt;em&gt;synchronously during &lt;code&gt;setup()&lt;/code&gt;&lt;/em&gt; are fine — &lt;a href="https://vuejs.org/guide/reusability/composables.html" rel="noopener noreferrer"&gt;Vue's docs&lt;/a&gt; recommend exactly that. The problem is when there's no component instance at all.)&lt;/li&gt;
&lt;li&gt;The timer fires 60 seconds after &lt;em&gt;load&lt;/em&gt;, not at the top of the minute. If a user opens your app at 10:00:58, the clock won't update until 10:01:58. Your app looks broken — the system tray says 10:01 but your UI still shows 10:00.&lt;/li&gt;
&lt;li&gt;The timer keeps running when the tab is hidden. Wasting CPU on a clock nobody is looking at.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Exhibit C: Scope-Safe Cleanup
&lt;/h2&gt;

&lt;p&gt;Vue has a method for scope-safe cleanup: &lt;a href="https://vuejs.org/api/reactivity-advanced.html#onscopedispose" rel="noopener noreferrer"&gt;&lt;code&gt;onScopeDispose&lt;/code&gt;&lt;/a&gt;. From the documentation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This method can be used as a non-component-coupled replacement of&lt;br&gt;
&lt;code&gt;onUnmounted&lt;/code&gt; in reusable composition functions, since each Vue component's&lt;br&gt;
&lt;code&gt;setup()&lt;/code&gt; function is also invoked in an effect scope.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The implementation is just a push onto the currently active scope's cleanup list. From the Vue core source (roughly — &lt;a href="https://github.com/vuejs/core/blob/main/packages/reactivity/src/effectScope.ts" rel="noopener noreferrer"&gt;&lt;code&gt;packages/reactivity/src/effectScope.ts&lt;/code&gt;&lt;/a&gt;):&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onScopeDispose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;failSilently&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&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="nx"&gt;activeEffectScope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;activeEffectScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cleanups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&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;__DEV__&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;failSilently&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`onScopeDispose() is called when there is no active effect scope...`&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;"Currently active" is the key phrase. Every component's &lt;code&gt;setup()&lt;/code&gt; runs inside an effect scope that Vue creates and activates for you, so inside a component the cleanup is automatic.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;onScopeDispose()&lt;/code&gt; cleans up automatically when &lt;code&gt;useNow()&lt;/code&gt; is called synchronously within an active Vue effect scope — such as component setup or setup-store creation. Outside one, callers must retain and call the returned &lt;code&gt;stop()&lt;/code&gt; or create their own scope.&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="c1"&gt;// In a component — Vue creates the scope, onScopeDispose just works&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useNow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pinia setup stores&lt;/strong&gt; also run inside their own effect scope. &lt;a href="https://pinia.vuejs.org/api/pinia/interfaces/StoreWithState.html#-dispose-" rel="noopener noreferrer"&gt;Pinia's &lt;code&gt;$dispose()&lt;/code&gt; method&lt;/a&gt; stops the store's associated effect scope (its &lt;a href="https://github.com/vuejs/pinia/blob/main/packages/pinia/src/store.ts" rel="noopener noreferrer"&gt;source&lt;/a&gt; calls &lt;code&gt;scope.stop()&lt;/code&gt;), which invokes any &lt;code&gt;onScopeDispose&lt;/code&gt; callbacks registered during setup. Note that unlike component unmount, &lt;code&gt;$dispose()&lt;/code&gt; must be called intentionally — it is not automatic.&lt;/p&gt;

&lt;p&gt;The only time you reach for &lt;code&gt;effectScope()&lt;/code&gt; yourself is when there's genuinely &lt;strong&gt;no&lt;/strong&gt; scope to hook into — app-level singletons, plugins, plain modules. In that case, either create a scope and remember to stop it:&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="c1"&gt;// No component, no store — create a scope, and remember to stop it&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;effectScope&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useNow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// triggers cleanup&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…or skip the scope and call &lt;code&gt;stop()&lt;/code&gt; manually:&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="c1"&gt;// No scope at all — caller disposes manually&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stop&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useNow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;// ...later, when the owner is torn down:&lt;/span&gt;
&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One caveat, straight from the &lt;a href="https://vuejs.org/api/composition-api-lifecycle.html#onscopedispose" rel="noopener noreferrer"&gt;Vue docs&lt;/a&gt;: if &lt;code&gt;onScopeDispose&lt;/code&gt; is called with &lt;strong&gt;no&lt;/strong&gt; active scope, Vue logs a warning and the callback is never registered. Vue 3.5+ accepts a second argument, &lt;code&gt;onScopeDispose(cleanup, true)&lt;/code&gt;, which suppresses the warning — but the callback is still not registered. That's why the final composable uses &lt;code&gt;failSilently = true&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; exposes &lt;code&gt;stop()&lt;/code&gt; for manual disposal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exhibit D: Clock Alignment + Visibility Pause
&lt;/h2&gt;

&lt;p&gt;The final composable (shown at the end of this article) addresses the human-perception problem and the battery-drain problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Clock Alignment
&lt;/h3&gt;

&lt;p&gt;If you load at 10:00:58, you don't want to wait 60 seconds for the first tick. You want the clock to update at 10:01:00 — aligned to the next interval boundary (minute boundaries with the default 60,000 ms interval).&lt;/p&gt;

&lt;p&gt;The composable reads the current time immediately on start, then schedules every subsequent tick at the next interval boundary. So the clock is correct from the moment it loads, and stays aligned on every tick.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;msUntilNextTick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;setTimeout&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="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Temporal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plainTimeISO&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setInterval&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="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Temporal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plainTimeISO&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;msUntilNextTick&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the default 60,000 ms interval, &lt;code&gt;msUntilNextTick&lt;/code&gt; is the milliseconds remaining until the next minute boundary. The initial &lt;code&gt;setTimeout&lt;/code&gt; fires at that boundary, then hands off to a recurring timer for all subsequent ticks. The clock feels synchronized with the system tray.&lt;/p&gt;

&lt;p&gt;But there's a subtlety: &lt;code&gt;setInterval&lt;/code&gt; drifts. Modern browsers under system load or low-power modes can slip a few milliseconds per tick. Over hours, the timer ends up firing 5–10 seconds past the top of the minute. The fix is a &lt;strong&gt;self-correcting recursive &lt;code&gt;setTimeout&lt;/code&gt;&lt;/strong&gt; that recalculates &lt;code&gt;interval - (Date.now() % interval)&lt;/code&gt; on &lt;em&gt;every&lt;/em&gt; tick — re-aligning to the real interval boundary each time. Delay does not accumulate from one tick to the next — if the browser runs a callback late, the next callback is scheduled toward the next clock boundary rather than inheriting the previous lateness.&lt;/p&gt;

&lt;h3&gt;
  
  
  Visibility Pause
&lt;/h3&gt;

&lt;p&gt;When the user switches tabs, &lt;code&gt;document.hidden&lt;/code&gt; becomes &lt;code&gt;true&lt;/code&gt;. We stop the timer. When they come back, we immediately sync the time and restart the interval.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onVisibilityChange&lt;/span&gt; &lt;span class="o"&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&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="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// immediately reads the current time, then starts ticking&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;No wasted CPU when nobody is looking. No stale time when they return.&lt;/p&gt;

&lt;h3&gt;
  
  
  SSR Safety
&lt;/h3&gt;

&lt;p&gt;If this composable runs during Server-Side Rendering (Nuxt, Vite SSG), &lt;code&gt;document&lt;/code&gt; doesn't exist. Calling &lt;code&gt;document.addEventListener&lt;/code&gt; at the top level would crash with &lt;code&gt;ReferenceError: document is not defined&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fix: wrap all DOM-dependent code in a &lt;code&gt;typeof window !== 'undefined'&lt;/code&gt; guard. The DOM code is SSR-safe. Separately, if you use Temporal, the server needs it too — natively or through a polyfill imported at your app's entry point. These are two independent requirements: no DOM on the server, and Temporal availability on the server.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Hydration note:&lt;/strong&gt; Rendering live time on the server can cause hydration&lt;br&gt;
mismatches because the server and browser render at different moments — and&lt;br&gt;
potentially in different time zones. The cleanest pattern for a live clock is&lt;br&gt;
to render a neutral placeholder server-side and start the actual clock after&lt;br&gt;
client mount.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Temporal Precision
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Temporal.Now.plainTimeISO()&lt;/code&gt; defaults to nanosecond precision (e.g., &lt;code&gt;14:30:15.123456789&lt;/code&gt;). When bound directly to a template, those sub-second digits render as noise. The fix: round to seconds with &lt;code&gt;.round({ smallestUnit: 'second' })&lt;/code&gt; so &lt;code&gt;now.value&lt;/code&gt; formats cleanly out of the box.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Verdict: The Final Composable
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;shallowRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onScopeDispose&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="s1"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isBrowser&lt;/span&gt; &lt;span class="o"&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="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&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;useNow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60000&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="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RangeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;interval must be a positive finite number of milliseconds&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="c1"&gt;// Round to seconds so default template rendering is clean (e.g., "14:30:15")&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getNow&lt;/span&gt; &lt;span class="o"&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="nx"&gt;Temporal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plainTimeISO&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;smallestUnit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;second&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// shallowRef: Temporal.PlainTime is immutable — no need for Vue to proxy&lt;/span&gt;
  &lt;span class="c1"&gt;// its internals. We only ever replace .value, never mutate it.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;shallowRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getNow&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stop&lt;/span&gt; &lt;span class="o"&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="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Self-correcting recursive timeout: recalculates remaining ms to next&lt;/span&gt;
  &lt;span class="c1"&gt;// boundary on every tick — delay does not accumulate across ticks&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scheduleNextUpdate&lt;/span&gt; &lt;span class="o"&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="nf"&gt;stop&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;msUntilNextTick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getNow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="nf"&gt;scheduleNextUpdate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;msUntilNextTick&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;start&lt;/span&gt; &lt;span class="o"&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="c1"&gt;// Don't schedule if the page is already hidden (e.g. loaded in a&lt;/span&gt;
    &lt;span class="c1"&gt;// background tab) — visibilitychange only fires on state changes&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;isBrowser&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&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;hidden&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;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getNow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;scheduleNextUpdate&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;onVisibilityChange&lt;/span&gt; &lt;span class="o"&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="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;start&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;dispose&lt;/span&gt; &lt;span class="o"&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="nf"&gt;stop&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;isBrowser&lt;/span&gt;&lt;span class="p"&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="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;visibilitychange&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onVisibilityChange&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="c1"&gt;// SSR Safe: only attach DOM listeners in the browser environment&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;isBrowser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;start&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="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;visibilitychange&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onVisibilityChange&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// failSilently=true: if no active scope, caller must call stop() manually&lt;/span&gt;
  &lt;span class="nf"&gt;onScopeDispose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dispose&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dispose&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;Usage in a component:&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="c1"&gt;// Vue creates the scope — onScopeDispose handles cleanup automatically&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useNow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Usage outside a scope (plugins, plain modules):&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="c1"&gt;// No active scope — caller disposes manually&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stop&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useNow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;// ...later, when the owner is torn down:&lt;/span&gt;
&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The composable handles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clock alignment to interval boundaries (minute boundaries with the default 60,000 ms interval; self-correcting, non-accumulating delay)&lt;/li&gt;
&lt;li&gt;Tab visibility pausing (including when the page loads already hidden)&lt;/li&gt;
&lt;li&gt;Scope-safe cleanup when called inside an active scope (components, Pinia setup stores, effectScope)&lt;/li&gt;
&lt;li&gt;Manual disposal via &lt;code&gt;stop()&lt;/code&gt; when called outside a scope&lt;/li&gt;
&lt;li&gt;SSR safety for DOM code (if you use Temporal, the server needs it too — import a polyfill at your app's entry point if needed)&lt;/li&gt;
&lt;li&gt;Clean Temporal formatting (rounded to seconds)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;shallowRef&lt;/code&gt; for immutable Temporal objects (no unnecessary proxying)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Many Clocks: Share One Instance
&lt;/h3&gt;

&lt;p&gt;Each &lt;code&gt;useNow()&lt;/code&gt; call creates its own timer and &lt;code&gt;visibilitychange&lt;/code&gt; listener. That's fine for one or a few clocks, but if a page renders many clock components, consider sharing a single app-level &lt;code&gt;useNow()&lt;/code&gt; instance via &lt;code&gt;effectScope()&lt;/code&gt; and passing the ref down via props or provide/inject.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Arguments: Why Recursive &lt;code&gt;setTimeout&lt;/code&gt; Instead of &lt;code&gt;setInterval&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;setInterval&lt;/code&gt;:&lt;/strong&gt; Fires on a fixed interval, but modern browsers drift under load or in low-power modes. A few milliseconds per tick compounds over hours — your clock ends up 5–10 seconds off the real interval boundary. Since we're building a &lt;em&gt;clock UI&lt;/em&gt;, that drift is visible to users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;setTimeout&lt;/code&gt; (recursive):&lt;/strong&gt; Each callback recalculates &lt;code&gt;interval - (Date.now() % interval)&lt;/code&gt; before scheduling the next tick. This re-aligns to the real interval boundary on &lt;em&gt;every&lt;/em&gt; tick. Delay does not accumulate — if the browser runs a callback late, the next callback targets the next clock boundary rather than inheriting the previous lateness. The callback reads &lt;code&gt;Temporal.Now.plainTimeISO()&lt;/code&gt; — instantaneous — so there's no risk of overlapping calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;requestAnimationFrame&lt;/code&gt;:&lt;/strong&gt; Fires ~60x/sec, synced to screen refresh. Designed for smooth animations. Way too frequent for a 60-second clock.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;nextTick&lt;/code&gt;:&lt;/strong&gt; Not a timer at all. Fires once, after Vue's DOM update cycle. No concept of time or repetition.&lt;/p&gt;

&lt;p&gt;For a clock that needs to stay in sync with the system tray over long sessions, self-correcting recursive &lt;code&gt;setTimeout&lt;/code&gt; is the right tool. Browser timer scheduling is &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#timeout_value_clamping" rel="noopener noreferrer"&gt;not exact&lt;/a&gt; — the event loop can be busy, and background tabs are commonly throttled — but self-correction ensures that lateness doesn't compound.&lt;/p&gt;

&lt;h2&gt;
  
  
  Expert Testimony: Why Temporal API Instead of &lt;code&gt;Date&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Date&lt;/code&gt; is perfectly reasonable for a simple display clock — especially if your app doesn't already depend on Temporal. But if your app already uses Temporal for its date/time semantics, it's the natural choice here.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Date&lt;/code&gt; object is mutable — &lt;code&gt;setHours()&lt;/code&gt;, &lt;code&gt;setDate()&lt;/code&gt;, &lt;code&gt;setMinutes()&lt;/code&gt; all modify the instance in place. This leads to subtle bugs when the same instance is shared across reactive state.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Temporal.PlainTime&lt;/code&gt; and &lt;code&gt;Temporal.PlainDate&lt;/code&gt; are &lt;strong&gt;immutable&lt;/strong&gt;. Every operation returns a new instance. No mutation bugs possible. &lt;code&gt;PlainTime&lt;/code&gt; represents wall-clock time without a date or time zone — exactly what a display clock needs.&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="c1"&gt;// Date — mutable, error-prone&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&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;Date&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="nf"&gt;setDate&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="nf"&gt;getDate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// tomorrow, but d is mutated in place&lt;/span&gt;

&lt;span class="c1"&gt;// Temporal — immutable, safe&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;today&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Temporal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;plainDateISO&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;tomorrow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;today&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="c1"&gt;// new instance, today unchanged&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As of mid-2026 it's formally standardized as Stage 4 and being merged into the &lt;a href="https://github.com/tc39/ecma262/pull/3759" rel="noopener noreferrer"&gt;ES2027 specification&lt;/a&gt; (the TC39 finished-proposals list updated the target publication year from 2026 to 2027). It's available in current Chromium and Firefox, but full cross-browser support still requires a fallback or polyfill — Safari and iOS Safari remain unsupported. The proposal's implementation status and &lt;a href="https://caniuse.com/temporal" rel="noopener noreferrer"&gt;caniuse&lt;/a&gt; agree:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chrome 144+&lt;/strong&gt; — shipped January 13, 2026&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firefox 139+&lt;/strong&gt; — shipped May 27, 2025&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge 144+&lt;/strong&gt; — Chromium-aligned&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safari&lt;/strong&gt; — Technology Preview only, disabled by default; the &lt;a href="https://bugs.webkit.org/show_bug.cgi?id=223166" rel="noopener noreferrer"&gt;WebKit implementation bug&lt;/a&gt; is still open&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js 26+&lt;/strong&gt; — shipped May 5, 2026&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you need to support browsers without native Temporal, import a Temporal polyfill at your app's entry point. Two options are &lt;a href="https://www.npmjs.com/package/@js-temporal/polyfill" rel="noopener noreferrer"&gt;&lt;code&gt;@js-temporal/polyfill&lt;/code&gt;&lt;/a&gt; (closest to the reference implementation, currently alpha) or &lt;a href="https://www.npmjs.com/package/temporal-polyfill" rel="noopener noreferrer"&gt;&lt;code&gt;temporal-polyfill&lt;/code&gt;&lt;/a&gt; (by FullCalendar, stable release). Pick the one that fits your needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case Summary
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;"Works" is not "works well."&lt;/strong&gt; The naive &lt;code&gt;setInterval&lt;/code&gt; ticks. But it leaks, drifts from real time, and wastes CPU on hidden tabs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Prefer &lt;code&gt;onScopeDispose&lt;/code&gt; for reusable composables.&lt;/strong&gt; It works inside any active Vue effect scope (components, Pinia setup stores, &lt;code&gt;effectScope()&lt;/code&gt;) — and inside components it's literally equivalent to &lt;code&gt;onUnmounted&lt;/code&gt;, per RFC #0041. But it's not magic: if there's no active scope, the callback is never registered. Always expose a manual &lt;code&gt;stop()&lt;/code&gt; for unmanaged usage.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Clock alignment is a human-perception problem.&lt;/strong&gt; Users don't know what "drift" means, but they know your clock looks wrong when it doesn't match their system tray.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Self-correcting beats fixed intervals.&lt;/strong&gt; &lt;code&gt;setInterval&lt;/code&gt; drifts under load. A recursive &lt;code&gt;setTimeout&lt;/code&gt; that recalculates alignment on every tick ensures delay doesn't accumulate — the next tick always targets the next real clock boundary.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Guard against SSR.&lt;/strong&gt; If your composable touches &lt;code&gt;document&lt;/code&gt; or &lt;code&gt;window&lt;/code&gt;, wrap it in &lt;code&gt;typeof window !== 'undefined'&lt;/code&gt;. And if you use Temporal, make sure the server has it too — natively or via polyfill.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;shallowRef&lt;/code&gt; for immutable objects.&lt;/strong&gt; &lt;code&gt;Temporal.PlainTime&lt;/code&gt; is&lt;br&gt;
immutable — you only ever replace &lt;code&gt;.value&lt;/code&gt;, never mutate its internals. &lt;code&gt;shallowRef&lt;/code&gt; avoids unnecessary proxy conversion and is the appropriate model for external class instances.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The court finds &lt;code&gt;setInterval&lt;/code&gt; guilty of negligence. The composable is released into production. Case closed. ⚖️&lt;/p&gt;

</description>
      <category>vue</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>The Case of the Lying Clock: 5 Vue Mysteries Solved</title>
      <dc:creator>Izak T</dc:creator>
      <pubDate>Thu, 13 Aug 2026 03:34:40 +0000</pubDate>
      <link>https://dev.to/kickbuttowski80/the-case-of-the-lying-clock-5-vue-mysteries-solved-1e99</link>
      <guid>https://dev.to/kickbuttowski80/the-case-of-the-lying-clock-5-vue-mysteries-solved-1e99</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu0atups87dowf8w853je.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu0atups87dowf8w853je.png" alt="A desk with a vintage clock and a magnifying glass on a wooden surface" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every detective has their cold cases. These are mine — five Vue concepts that&lt;br&gt;
confused me until I investigated them properly. Grab your magnifying glass.&lt;/p&gt;


&lt;h2&gt;
  
  
  Case #1: The Lying Clock
&lt;/h2&gt;

&lt;p&gt;Imagine you set an alarm to go off every 60 seconds. You press start at&lt;br&gt;
10:00:00.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First alarm: 10:01:00 — perfect&lt;/li&gt;
&lt;li&gt;Second alarm: 10:02:00 — still good&lt;/li&gt;
&lt;li&gt;But your phone is also doing other things: checking email, refreshing
weather, running background tasks. Sometimes it fires the alarm a tiny bit
late.&lt;/li&gt;
&lt;li&gt;Third alarm: 10:03:01 (1 second late)&lt;/li&gt;
&lt;li&gt;Fourth alarm: 10:04:02 (2 seconds off now)&lt;/li&gt;
&lt;li&gt;Five hours later: your alarm fires at 15:05:12 when it should fire at
15:05:00&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gap growing bigger over time — that's &lt;strong&gt;drift&lt;/strong&gt;. The timer slowly slides&lt;br&gt;
away from where it should be.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why Does This Happen?
&lt;/h3&gt;

&lt;p&gt;JavaScript runs on a single thread — it can only do one thing at a time. When a&lt;br&gt;
timer is supposed to fire, the browser puts it in a queue. But if the thread is&lt;br&gt;
busy doing something else, the timer waits. The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#reasons_for_delays_longer_than_specified" rel="noopener noreferrer"&gt;MDN documentation for&lt;br&gt;
&lt;code&gt;setTimeout&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
lists several reasons timers fire late:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nested timeouts&lt;/strong&gt; are throttled to a minimum of 4ms after 5 levels of
nesting (per the &lt;a href="https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timers" rel="noopener noreferrer"&gt;HTML5 spec&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background tabs&lt;/strong&gt; are throttled to a maximum of once per second
(&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#timeouts_in_inactive_tabs_throttled_to_=1000ms" rel="noopener noreferrer"&gt;MDN&lt;/a&gt;:
"timeouts are throttled to firing no more often than once per second (1000 ms)
in inactive tabs")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chrome 88+&lt;/strong&gt; introduced &lt;a href="https://developer.chrome.com/blog/timer-throttling-in-chrome-88" rel="noopener noreferrer"&gt;intensive throttling&lt;/a&gt;
for hidden pages: timers that have been hidden for more than 5 minutes are
checked only once per minute&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tracking scripts&lt;/strong&gt; in Firefox get even more aggressive throttling: 10
second minimum in background tabs&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Does This Happen on New Devices Too?
&lt;/h3&gt;

&lt;p&gt;Yes, but less. Modern devices are faster, so the delay per tick is smaller —&lt;br&gt;
maybe 1-2 milliseconds instead of 10-20. But over hours, even 1ms per tick&lt;br&gt;
adds up. And background tab throttling happens on &lt;em&gt;every&lt;/em&gt; device, no matter how&lt;br&gt;
fast — it's a browser policy, not a hardware limitation.&lt;/p&gt;
&lt;h3&gt;
  
  
  Is This Common Knowledge?
&lt;/h3&gt;

&lt;p&gt;It's the kind of thing you learn when your boss says "why does the clock on&lt;br&gt;
our dashboard show the wrong time after lunch?" and you Google it. Many&lt;br&gt;
developers use &lt;code&gt;setInterval&lt;/code&gt; and never notice because their app doesn't stay&lt;br&gt;
open long enough for the drift to matter.&lt;/p&gt;


&lt;h2&gt;
  
  
  Case #1 Continued: How Recursive &lt;code&gt;setTimeout&lt;/code&gt; Fixes the Drift
&lt;/h2&gt;

&lt;p&gt;A fixed interval can fall behind its intended clock boundary when callbacks&lt;br&gt;
run late — the lateness piles up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Start at 10:00:00
→ fire at 10:01:00.002 (2ms late)
→ fire at 10:02:00.005 (5ms late)
→ fire at 10:03:00.009 (getting worse)
→ ...hours later, you're 10+ seconds off
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Recursive &lt;code&gt;setTimeout&lt;/code&gt;&lt;/strong&gt; recalculates the next boundary from wall-clock time&lt;br&gt;
each tick, so delay doesn't accumulate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Start at 10:00:00
→ "next minute is 10:01:00, that's 60000ms away" → set timeout for 60000ms
→ fire at 10:01:00.002 (2ms late — same as before)
→ BUT NOW: check clock: "it's 10:01:00.002, next minute is 10:02:00, that's 59998ms"
→ set timeout for 59998ms
→ fire at 10:02:00.001 (only 1ms late! it corrected itself!)
→ check clock: "it's 10:02:00.001, next minute is 10:03:00, that's 59999ms"
→ fire at 10:03:00.000 — right on time
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's called "recursive" because the function calls itself — inside the&lt;br&gt;
&lt;code&gt;setTimeout&lt;/code&gt; callback, we call &lt;code&gt;scheduleNextTick()&lt;/code&gt; again, which sets up the&lt;br&gt;
next &lt;code&gt;setTimeout&lt;/code&gt;. It's a loop that builds itself one step at a time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analogy:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;setInterval&lt;/code&gt; = a runner who counts their steps and hopes they're on pace&lt;/li&gt;
&lt;li&gt;Recursive &lt;code&gt;setTimeout&lt;/code&gt; = a runner who checks their watch before every step&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Case #2: The Box That Didn't Need Tracking — &lt;code&gt;shallowRef&lt;/code&gt; vs &lt;code&gt;ref&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;When you put an object inside &lt;code&gt;ref()&lt;/code&gt;, Vue wraps every single property in a&lt;br&gt;
reactive proxy. If your object has &lt;code&gt;{ hour: 14, minute: 30, second: 15 }&lt;/code&gt;, Vue&lt;br&gt;
tracks all three properties — plus every method, plus any nested objects. This&lt;br&gt;
is great when you need to change one property and have the UI update.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;Temporal.PlainTime&lt;/code&gt; is &lt;strong&gt;immutable&lt;/strong&gt; — you can never change its properties.&lt;br&gt;
You can only throw the whole thing away and make a new one. So Vue's deep&lt;br&gt;
tracking is wasted effort.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;shallowRef&lt;/code&gt; says: "only track &lt;code&gt;.value&lt;/code&gt; itself, don't go inside." The &lt;a href="https://vuejs.org/api/reactivity-advanced.html#shallowref" rel="noopener noreferrer"&gt;Vue&lt;br&gt;
documentation&lt;/a&gt; puts&lt;br&gt;
it plainly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Unlike &lt;code&gt;ref()&lt;/code&gt;, the inner value of a shallow ref is stored and exposed as-is,&lt;br&gt;
and will not be made deeply reactive. Only the &lt;code&gt;.value&lt;/code&gt; access is reactive.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And the docs even tell you when to use it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;shallowRef()&lt;/code&gt; is typically used for performance optimizations of large data&lt;br&gt;
structures, or integration with external state management systems.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Since we only ever do &lt;code&gt;now.value = getNow()&lt;/code&gt; (replace the whole object) and&lt;br&gt;
never mutate internal properties, &lt;code&gt;shallowRef&lt;/code&gt; does exactly what we need — and&lt;br&gt;
nothing we don't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analogy:&lt;/strong&gt; &lt;code&gt;ref&lt;/code&gt; is like putting a GPS tracker on every item inside a box.&lt;br&gt;
&lt;code&gt;shallowRef&lt;/code&gt; is like putting a tracker only on the box. If you never open the&lt;br&gt;
box and change items inside — you only swap the whole box — the single tracker&lt;br&gt;
is enough.&lt;/p&gt;


&lt;h2&gt;
  
  
  Case #3: The Cleanup That Worked Everywhere — Effect Scopes and &lt;code&gt;onScopeDispose&lt;/code&gt;
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;When a component unmounts, Vue needs to clean up — stop timers, remove event&lt;br&gt;
listeners, cancel watchers. &lt;code&gt;onUnmounted&lt;/code&gt; is the hook for this. But it only&lt;br&gt;
works inside components. If you use a composable from a Pinia store or a&lt;br&gt;
standalone module, &lt;code&gt;onUnmounted&lt;/code&gt; never fires — there's no component to unmount.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Solution
&lt;/h3&gt;

&lt;p&gt;Vue 3.2 introduced &lt;code&gt;effectScope&lt;/code&gt; and &lt;code&gt;onScopeDispose&lt;/code&gt; via&lt;br&gt;
&lt;a href="https://github.com/vuejs/rfcs/blob/master/active-rfcs/0041-reactivity-effect-scope.md" rel="noopener noreferrer"&gt;RFC #0041&lt;/a&gt;.&lt;br&gt;
The RFC explains the motivation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In Vue's component &lt;code&gt;setup()&lt;/code&gt;, effects will be collected and bound to the&lt;br&gt;
current instance. When the instance gets unmounted, effects will be disposed&lt;br&gt;
automatically. This RFC is trying to abstract the component's &lt;code&gt;setup()&lt;/code&gt;&lt;br&gt;
effect collecting and disposing feature into a more reusable API that can be&lt;br&gt;
used outside of the component model.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In plain English: Vue already had this cleanup mechanism inside components. They&lt;br&gt;
just made it available outside components too.&lt;/p&gt;
&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;p&gt;Every component's &lt;code&gt;setup()&lt;/code&gt; runs inside an &lt;strong&gt;effect scope&lt;/strong&gt; — Vue creates it&lt;br&gt;
automatically. &lt;code&gt;onScopeDispose&lt;/code&gt; registers a cleanup callback on whatever scope&lt;br&gt;
is currently active. The &lt;a href="https://vuejs.org/api/reactivity-advanced.html#onscopedispose" rel="noopener noreferrer"&gt;Vue docs&lt;/a&gt;&lt;br&gt;
describe it as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Registers a dispose callback on the current active effect scope. This method&lt;br&gt;
can be used as a non-component-coupled replacement of &lt;code&gt;onUnmounted&lt;/code&gt; in&lt;br&gt;
reusable composition functions, since each Vue component's &lt;code&gt;setup()&lt;/code&gt; function&lt;br&gt;
is also invoked in an effect scope.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So inside a component, &lt;code&gt;onScopeDispose&lt;/code&gt; and &lt;code&gt;onUnmounted&lt;/code&gt; do the same thing.&lt;br&gt;
But &lt;code&gt;onScopeDispose&lt;/code&gt; also works in Pinia stores and standalone scopes.&lt;/p&gt;
&lt;h3&gt;
  
  
  Will &lt;code&gt;onMounted&lt;/code&gt; and &lt;code&gt;onUnmounted&lt;/code&gt; Go Away?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;onMounted&lt;/code&gt;&lt;/strong&gt; — stays forever. It's about the DOM being ready, not about
cleanup. Neither &lt;code&gt;effectScope&lt;/code&gt; nor &lt;code&gt;onScopeDispose&lt;/code&gt; can replace it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;onUnmounted&lt;/code&gt;&lt;/strong&gt; — mostly replaced by &lt;code&gt;onScopeDispose&lt;/code&gt; for cleanup in
composables. It still works, but &lt;code&gt;onScopeDispose&lt;/code&gt; is more versatile.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  What Happens If You Use Both Inside a Component?
&lt;/h3&gt;

&lt;p&gt;Nothing bad — they both fire during teardown. No conflict, no error.&lt;br&gt;
It's just redundant — using both for the same cleanup is unnecessary.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Catch: No Active Scope
&lt;/h3&gt;

&lt;p&gt;If &lt;code&gt;onScopeDispose&lt;/code&gt; is called with no active scope (like from a plain module),&lt;br&gt;
Vue logs a warning and the callback is &lt;strong&gt;never registered&lt;/strong&gt;. The&lt;br&gt;
&lt;a href="https://github.com/vuejs/core/blob/main/packages/reactivity/src/effectScope.ts" rel="noopener noreferrer"&gt;Vue source&lt;/a&gt;&lt;br&gt;
shows exactly why:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onScopeDispose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;failSilently&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&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="nx"&gt;activeEffectScope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;activeEffectScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cleanups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&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;failSilently&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`onScopeDispose() is called when there is no active effect scope...`&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;activeEffectScope&lt;/code&gt; → no push → no cleanup. That's why our composable uses&lt;br&gt;
&lt;code&gt;failSilently = true&lt;/code&gt; (to suppress the warning) &lt;strong&gt;and&lt;/strong&gt; exposes a &lt;code&gt;stop()&lt;/code&gt;&lt;br&gt;
function (so the caller can clean up manually).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analogy:&lt;/strong&gt; &lt;code&gt;onUnmounted&lt;/code&gt; is like "tell me when the building is demolished."&lt;br&gt;
&lt;code&gt;onScopeDispose&lt;/code&gt; is like "tell me when my &lt;em&gt;room&lt;/em&gt; is demolished" — works whether&lt;br&gt;
your room is in a building, a store, or a standalone structure. But if you're&lt;br&gt;
not in any room at all, nobody will tell you anything — so keep a fire&lt;br&gt;
extinguisher (&lt;code&gt;stop()&lt;/code&gt;) handy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Case #4: The Server That Knew Too Much — Hydration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Short Version
&lt;/h3&gt;

&lt;p&gt;When using SSR (Server-Side Rendering, like Nuxt), the server generates complete&lt;br&gt;
HTML and sends it to the browser. The user sees a full page immediately. Then&lt;br&gt;
Vue loads in the browser and "takes over" that HTML — attaching event listeners,&lt;br&gt;
making it interactive. That takeover is &lt;strong&gt;hydration&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why It Matters for Clocks
&lt;/h3&gt;

&lt;p&gt;The server renders at one moment — say 10:00:00. That HTML travels across the&lt;br&gt;
internet for 500ms. The browser hydrates at 10:00:01. Vue compares the server&lt;br&gt;
HTML to what it wants to render and finds a mismatch: the server wrote 10:00:00&lt;br&gt;
but the browser wants 10:00:01.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://nuxt.com/docs/3.x/guide/best-practices/hydration" rel="noopener noreferrer"&gt;Nuxt hydration guide&lt;/a&gt;&lt;br&gt;
lists this as a common problem:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Content that changes based on current time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And &lt;a href="https://www.nazarboyko.com/articles/hydration-issues-in-vue-and-nuxt" rel="noopener noreferrer"&gt;Nazar Boyko's deep dive on hydration&lt;/a&gt;&lt;br&gt;
explains why it matters:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The server runs this code at, say, 14:32:01. The client runs it at 14:32:03.&lt;br&gt;
The text nodes do not match, and Vue complains.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It gets worse: the server might be in a different time zone than the user. A&lt;br&gt;
server in New York rendering 10:00:00 EST is 15:00:00 for a user in Tokyo.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fix
&lt;/h3&gt;

&lt;p&gt;The cleanest pattern is to render a placeholder on the server and start the real&lt;br&gt;
clock only after the browser takes over. As the&lt;br&gt;
&lt;a href="https://masteringnuxt.com/blog/fixing-hydration-errors-in-nuxt-a-practical-guide" rel="noopener noreferrer"&gt;Mastering Nuxt guide&lt;/a&gt;&lt;br&gt;
recommends:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Render time only on the client.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or use Nuxt's &lt;a href="https://nuxt.com/docs/api/components/client-only" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;ClientOnly&amp;gt;&lt;/code&gt;&lt;/a&gt;&lt;br&gt;
component, or the&lt;br&gt;
&lt;a href="https://github.com/danielroe/nuxt-time" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;NuxtTime&amp;gt;&lt;/code&gt;&lt;/a&gt; component which is&lt;br&gt;
specifically designed for SSR-safe time rendering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Analogy:&lt;/strong&gt; It's like a restaurant menu printed yesterday with "Today's Special:&lt;br&gt;
Soup." When you read it today, it's wrong. Better to leave the special blank on&lt;br&gt;
the printed menu and write it in by hand when the customer sits down.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does This Apply to Every Project?
&lt;/h3&gt;

&lt;p&gt;No — if you're using plain Vite + Vue, not SSR, there's no hydration. But a&lt;br&gt;
good composable handles it anyway with the &lt;code&gt;typeof window !== 'undefined'&lt;/code&gt;&lt;br&gt;
guard, which is good practice for reusable code that someone might use in Nuxt&lt;br&gt;
someday.&lt;/p&gt;




&lt;h2&gt;
  
  
  Case #5: The Warning Nobody Needed — &lt;code&gt;failSilently&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;When &lt;code&gt;onScopeDispose&lt;/code&gt; is called with no active scope, Vue logs a warning:&lt;br&gt;
"Hey, there's no scope to attach this to!" That warning can be noisy if you&lt;br&gt;
&lt;em&gt;intentionally&lt;/em&gt; call the composable outside a scope.&lt;/p&gt;

&lt;p&gt;Passing &lt;code&gt;true&lt;/code&gt; as the second argument tells Vue: "I know there might not be a&lt;br&gt;
scope. Don't warn me. I'll handle cleanup myself with &lt;code&gt;stop()&lt;/code&gt;."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; &lt;code&gt;failSilently = true&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; create cleanup. It just hides&lt;br&gt;
the warning. The callback is still not registered. You still need &lt;code&gt;stop()&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Mini-note: Temporal Rounding&lt;/strong&gt; — &lt;code&gt;Temporal.Now.plainTimeISO()&lt;/code&gt; gives you&lt;br&gt;
nanosecond precision (&lt;code&gt;14:30:15.123456789&lt;/code&gt;). That's nine digits after the&lt;br&gt;
seconds. &lt;code&gt;.round({ smallestUnit: 'second' })&lt;/code&gt; trims it to &lt;code&gt;14:30:15&lt;/code&gt;. Clean.&lt;br&gt;
Like showing "$14.30" instead of "$14.300000001."&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Case Closed
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Timer drift&lt;/strong&gt; — A fixed interval can fall behind its intended clock boundary; recursive &lt;code&gt;setTimeout&lt;/code&gt; recalculates from wall-clock time each tick&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;shallowRef&lt;/code&gt;&lt;/strong&gt; — Only tracks &lt;code&gt;.value&lt;/code&gt; replacement, not internal property changes — perfect for immutable objects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Effect scopes + &lt;code&gt;onScopeDispose&lt;/code&gt;&lt;/strong&gt; — Scopes group reactive effects for disposal; &lt;code&gt;onScopeDispose&lt;/code&gt; registers cleanup on the active scope — works in components, setup-store creation, and manual scopes, but not with no scope at all&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hydration&lt;/strong&gt; — Server sends HTML, browser "takes over" — time-based content can mismatch between the two&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;failSilently&lt;/code&gt;&lt;/strong&gt; — Suppresses the "no active scope" warning; does NOT create cleanup — you still need &lt;code&gt;stop()&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>vue3</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Stop Using Props + Watchers Just to Trigger Actions in Child Components (Vue 3.5 useTemplateRef + defineExpose)</title>
      <dc:creator>Izak T</dc:creator>
      <pubDate>Fri, 07 Aug 2026 22:41:36 +0000</pubDate>
      <link>https://dev.to/kickbuttowski80/stop-using-props-watchers-just-to-trigger-actions-in-child-components-vue-35-usetemplateref--5gdk</link>
      <guid>https://dev.to/kickbuttowski80/stop-using-props-watchers-just-to-trigger-actions-in-child-components-vue-35-usetemplateref--5gdk</guid>
      <description>&lt;p&gt;How many times have you seen (or written) code where a parent component passes a boolean prop to a child component solely to trigger a method inside that child?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- ❌ Parent passing a trigger flag --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;ChildComponent&lt;/span&gt; &lt;span class="na"&gt;:shouldReset=&lt;/span&gt;&lt;span class="s"&gt;"isResetting"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the child component, you end up writing an artificial watcher just to catch that state flip:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- ❌ Child watching a prop just to run a method --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt;&lt;span class="nt"&gt;&amp;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;watch&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="s1"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;defineProps&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;shouldReset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&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;watch&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;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shouldReset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newVal&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="nx"&gt;newVal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;resetForm&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// Running internal logic based on prop change&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a classic Vue code smell. Passing state down when you actually mean to send a &lt;strong&gt;command&lt;/strong&gt; creates awkward state-toggling hacks, extra reactivity cycles, and unnecessary watcher logic.&lt;/p&gt;

&lt;p&gt;Here is how to clean up this anti-pattern using &lt;code&gt;defineExpose&lt;/code&gt; and Vue 3.5’s &lt;code&gt;useTemplateRef()&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why "Props + Watchers" as Action Triggers is a Smell
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Artificial State Pollution:&lt;/strong&gt; You have to manage boolean flags in the parent (&lt;code&gt;isResetting = true&lt;/code&gt;, then back to &lt;code&gt;false&lt;/code&gt;) just to fire a one-time event.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reactivity Overhead:&lt;/strong&gt; Vue has to track the prop dependency, queue the watcher, evaluate the callback, and schedule component re-renders—all to invoke a simple function.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cluttered Child Logic:&lt;/strong&gt; The child component needs boilerplate code to watch a prop, handle boolean states, and reset the flag.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Vue is primarily declarative, but &lt;strong&gt;UI actions are inherently imperative&lt;/strong&gt;. Focusing an input, resetting a form, playing an animation, or stepping through a wizard are actions, not state.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Better Pattern: &lt;code&gt;defineExpose&lt;/code&gt; + &lt;code&gt;useTemplateRef&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Instead of forcing declarative props onto an imperative action, expose the child’s internal function directly to the parent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Explicitly Expose the Method in the Child
&lt;/h3&gt;

&lt;p&gt;By default, components using &lt;code&gt;&amp;lt;script setup&amp;gt;&lt;/code&gt; are &lt;strong&gt;closed&lt;/strong&gt;. External components cannot access internal variables or functions unless you explicitly expose them using &lt;code&gt;defineExpose&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- ChildComponent.vue --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt;&lt;span class="nt"&gt;&amp;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;ref&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="s1"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// The internal action&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resetForm&lt;/span&gt; &lt;span class="o"&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="nx"&gt;formState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Form reset executed!&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="c1"&gt;// Explicitly grant access to external parents&lt;/span&gt;
&lt;span class="nf"&gt;defineExpose&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;resetForm&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;form&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"formState.name"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Name"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;v-model=&lt;/span&gt;&lt;span class="s"&gt;"formState.email"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Email"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Call the Exposed Method in the Parent (Vue 3.5)
&lt;/h3&gt;

&lt;p&gt;In Vue 3.5+, use &lt;code&gt;useTemplateRef()&lt;/code&gt; to obtain a strongly-typed reference to the child component instance, then call the method directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- ParentComponent.vue --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt;&lt;span class="nt"&gt;&amp;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;useTemplateRef&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="s1"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ChildComponent&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./ChildComponent.vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// Obtain the reference using Vue 3.5's useTemplateRef&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;childRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useTemplateRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;childRef&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;handleResetButtonClick&lt;/span&gt; &lt;span class="o"&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="c1"&gt;// Directly invoke the exposed method on the child instance&lt;/span&gt;
  &lt;span class="nx"&gt;childRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;resetForm&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ChildComponent&lt;/span&gt; &lt;span class="na"&gt;ref=&lt;/span&gt;&lt;span class="s"&gt;"childRef"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"handleResetButtonClick"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Reset Child Form&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Architectural Balance: When to Use What
&lt;/h2&gt;

&lt;p&gt;A common rule in Vue is &lt;em&gt;"Props Down, Events Up"&lt;/em&gt;. Reaching into child components with refs should not replace standard data flow, but knowing when to use each pattern keeps your architecture clean:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Recommended Pattern&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Passing data into a child&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Props&lt;/strong&gt; (&lt;code&gt;:data="..."&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Declarative state rendering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notifying a parent about child actions&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Emits&lt;/strong&gt; (&lt;code&gt;@submit="..."&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Decoupled event notification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Triggering imperative child UI actions&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;defineExpose&lt;/code&gt; + `useTemplateRef&lt;/strong&gt;`&lt;/td&gt;
&lt;td&gt;Direct command execution without state hacks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;If your component requires a prop and a &lt;code&gt;watch&lt;/code&gt; solely to trigger an internal function, you are using reactivity as a workaround for command execution.&lt;/p&gt;

&lt;p&gt;Leverage &lt;code&gt;defineExpose&lt;/code&gt; alongside Vue 3.5's &lt;code&gt;useTemplateRef()&lt;/code&gt; to keep component logic clean, imperative, and free of artificial state flags.&lt;/p&gt;

</description>
      <category>vue</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>frontend</category>
    </item>
  </channel>
</rss>
