<?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, 13 Aug 2026 15:17:42 +0000</pubDate>
      <link>https://dev.to/kickbuttowski80/perry-mason-in-the-case-of-the-drifting-timer-25bj</link>
      <guid>https://dev.to/kickbuttowski80/perry-mason-in-the-case-of-the-drifting-timer-25bj</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&lt;br&gt;
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&lt;br&gt;
"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 —&lt;br&gt;
and the four iterations it took to get there. The prosecution calls four&lt;br&gt;
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&lt;br&gt;
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: The Cleanup That Failed
&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.&lt;br&gt;
A step forward — but the prosecution has three more objections:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Further evidence:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This 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 3.2 introduced &lt;code&gt;onScopeDispose&lt;/code&gt; — proposed in&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: Reactivity Effect Scope&lt;/a&gt;&lt;br&gt;
and shipped in &lt;a href="https://github.com/vuejs/core/releases/tag/v3.2.0" rel="noopener noreferrer"&gt;Vue 3.2 (August 2021)&lt;/a&gt;.&lt;br&gt;
The RFC describes it as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The global hook &lt;code&gt;onScopeDispose()&lt;/code&gt; serves a similar functionality to&lt;br&gt;
&lt;code&gt;onUnmounted()&lt;/code&gt;, but works for the current scope instead of the component&lt;br&gt;
instance. This could benefit composable functions to clean up their side&lt;br&gt;
effects along with its scope. &lt;strong&gt;Since &lt;code&gt;setup()&lt;/code&gt; also creates a scope for the&lt;br&gt;
component, it will be equivalent to &lt;code&gt;onUnmounted()&lt;/code&gt; when there is no explicit&lt;br&gt;
effect scope created.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;— Vue RFC #0041, "Reactivity Effect Scope"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's not marketing — the implementation really is just a push onto the&lt;br&gt;
currently active scope's cleanup list. From the Vue core source&lt;br&gt;
(&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="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 to be associated with.`&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&lt;br&gt;
effect scope that Vue creates and activates for you — before invoking your setup&lt;br&gt;
code, &lt;code&gt;setupComponent&lt;/code&gt; routes through &lt;code&gt;setCurrentInstance&lt;/code&gt;, which calls&lt;br&gt;
&lt;code&gt;instance.scope.on()&lt;/code&gt; — so inside a component the cleanup is automatic. The RFC's&lt;br&gt;
own example makes the guarantee explicit:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This still works because a Vue component now also runs its &lt;code&gt;setup()&lt;/code&gt; inside a&lt;br&gt;
scope, which will be disposed when the component is unmounted.&lt;/p&gt;

&lt;p&gt;— Vue RFC #0041, "Reactivity Effect Scope"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;onScopeDispose()&lt;/code&gt; cleans up automatically when &lt;code&gt;useNow()&lt;/code&gt; is called&lt;br&gt;
synchronously within an active Vue effect scope — such as component setup or&lt;br&gt;
setup-store creation. Outside one, callers must retain and call the returned&lt;br&gt;
&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. The&lt;br&gt;
&lt;a href="https://pinia.vuejs.org/api/pinia/interfaces/StoreWithState.html" rel="noopener noreferrer"&gt;Pinia &lt;code&gt;$dispose()&lt;/code&gt; docs&lt;/a&gt;&lt;br&gt;
confirm: calling &lt;code&gt;store.$dispose()&lt;/code&gt; stops the store's associated effect scope,&lt;br&gt;
which triggers any &lt;code&gt;onScopeDispose&lt;/code&gt; callbacks registered during setup. Note that&lt;br&gt;
unlike component unmount, &lt;code&gt;$dispose()&lt;/code&gt; must be called intentionally — it is not&lt;br&gt;
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;br&gt;
&lt;strong&gt;no&lt;/strong&gt; scope to hook into — app-level singletons, plugins, plain modules. In&lt;br&gt;
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;br&gt;
&lt;a href="https://vuejs.org/api/composition-api-lifecycle.html#onscopedispose" rel="noopener noreferrer"&gt;Vue docs&lt;/a&gt;:&lt;br&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&lt;br&gt;
the callback is never registered. Vue 3.5+ accepts a second argument,&lt;br&gt;
&lt;code&gt;onScopeDispose(cleanup, true)&lt;/code&gt;, which suppresses the warning — but the&lt;br&gt;
callback is still not registered. That's why the final composable uses&lt;br&gt;
&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 version addresses the human-perception problem and the battery-drain&lt;br&gt;
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.&lt;br&gt;
You want the clock to update at 10:01:00 — aligned to the next interval&lt;br&gt;
boundary (minute boundaries with the default 60,000 ms 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;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;The initial &lt;code&gt;setTimeout&lt;/code&gt; fires at the next interval boundary, then hands off to&lt;br&gt;
a recurring timer for all subsequent ticks. The clock feels synchronized with&lt;br&gt;
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&lt;br&gt;
load or low-power modes can slip a few milliseconds per tick. Over hours, the&lt;br&gt;
timer ends up firing 5–10 seconds past the top of the minute. The fix is a&lt;br&gt;
&lt;strong&gt;self-correcting recursive &lt;code&gt;setTimeout&lt;/code&gt;&lt;/strong&gt; that recalculates&lt;br&gt;
&lt;code&gt;interval - (Date.now() % interval)&lt;/code&gt; on &lt;em&gt;every&lt;/em&gt; tick — re-aligning to the&lt;br&gt;
real interval boundary each time. Delay does not accumulate from one tick to&lt;br&gt;
the next — if the browser runs a callback late, the next callback is scheduled&lt;br&gt;
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&lt;br&gt;
timer. When they come back, we immediately sync the time and restart the&lt;br&gt;
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;br&gt;
&lt;code&gt;document&lt;/code&gt; doesn't exist. Calling &lt;code&gt;document.addEventListener&lt;/code&gt; at the top level&lt;br&gt;
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;&lt;br&gt;
guard. The DOM code is SSR-safe. To make the composable fully SSR-safe, the&lt;br&gt;
server and target browsers must also provide Temporal, either natively or&lt;br&gt;
through an explicitly imported polyfill.&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&lt;br&gt;
(e.g., &lt;code&gt;14:30:15.123456789&lt;/code&gt;). When bound directly to a template, those&lt;br&gt;
sub-second digits render as noise. The fix: round to seconds with&lt;br&gt;
&lt;code&gt;.round({ smallestUnit: 'second' })&lt;/code&gt; so &lt;code&gt;now.value&lt;/code&gt; formats cleanly out of the&lt;br&gt;
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="c1"&gt;// If Temporal isn't available natively (e.g. Safari), import a polyfill first:&lt;/span&gt;
&lt;span class="c1"&gt;// import { Temporal } from '@js-temporal/polyfill'&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 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;scheduleNextTick&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;scheduleNextTick&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;document&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="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;scheduleNextTick&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;document&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="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="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="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 (Temporal must be available on the server too —
import a polyfill 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;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Many clocks on one page?&lt;/strong&gt; Each &lt;code&gt;useNow()&lt;/code&gt; call creates its own timer and&lt;br&gt;
&lt;code&gt;visibilitychange&lt;/code&gt; listener. That's fine for one or a few clocks, but if a&lt;br&gt;
page renders many clock components, consider sharing a single app-level&lt;br&gt;
&lt;code&gt;useNow()&lt;/code&gt; instance via &lt;code&gt;effectScope()&lt;/code&gt; and passing the ref down via props or&lt;br&gt;
provide/inject.&lt;/p&gt;
&lt;/blockquote&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&lt;br&gt;
load or in low-power modes. A few milliseconds per tick compounds over hours —&lt;br&gt;
your clock ends up 5–10 seconds off the real interval boundary. Since we're&lt;br&gt;
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;br&gt;
&lt;code&gt;interval - (Date.now() % interval)&lt;/code&gt; before scheduling the next tick. This&lt;br&gt;
re-aligns to the real interval boundary on &lt;em&gt;every&lt;/em&gt; tick. Delay does not&lt;br&gt;
accumulate — if the browser runs a callback late, the next callback targets&lt;br&gt;
the next clock boundary rather than inheriting the previous lateness. The&lt;br&gt;
callback reads &lt;code&gt;Temporal.Now.plainTimeISO()&lt;/code&gt; — instantaneous — so there's no&lt;br&gt;
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&lt;br&gt;
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&lt;br&gt;
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,&lt;br&gt;
self-correcting recursive &lt;code&gt;setTimeout&lt;/code&gt; is the right tool. Browser timer&lt;br&gt;
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;&lt;br&gt;
— the event loop can be busy, and background tabs are commonly throttled — but&lt;br&gt;
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&lt;br&gt;
app doesn't already depend on Temporal. But if your app already uses Temporal&lt;br&gt;
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&lt;br&gt;
modify the instance in place. This leads to subtle bugs when the same instance&lt;br&gt;
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&lt;br&gt;
returns a new instance. No mutation bugs possible. &lt;code&gt;PlainTime&lt;/code&gt; represents&lt;br&gt;
wall-clock time without a date or time zone — exactly what a display clock&lt;br&gt;
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;setHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// mutates d 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="c1"&gt;// no time component to zero out&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;// returns new instance, today unchanged&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Temporal's status, per the&lt;br&gt;
&lt;a href="https://github.com/tc39/proposal-temporal" rel="noopener noreferrer"&gt;official TC39 proposal&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This proposal is currently Stage 4. It will be merged into the ECMA-262 and&lt;br&gt;
ECMA-402 standards and this repository will be archived.&lt;/p&gt;

&lt;p&gt;— tc39/proposal-temporal&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As of mid-2026 it's formally standardized as Stage 4 and being merged into the&lt;br&gt;
&lt;a href="https://github.com/tc39/ecma262/pull/3759" rel="noopener noreferrer"&gt;ES2027 specification&lt;/a&gt; (the TC39&lt;br&gt;
finished-proposals list updated the target publication year from 2026 to 2027).&lt;br&gt;
It's available in current Chromium and Firefox, but production cross-browser&lt;br&gt;
support still requires a fallback or polyfill — Safari and iOS Safari remain&lt;br&gt;
unsupported. The proposal's implementation status and&lt;br&gt;
&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, drop in a polyfill such&lt;br&gt;
as &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;&lt;br&gt;
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;.&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,&lt;br&gt;
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&lt;br&gt;
active Vue effect scope (components, Pinia setup stores, &lt;code&gt;effectScope()&lt;/code&gt;) —&lt;br&gt;
and inside components it's literally equivalent to &lt;code&gt;onUnmounted&lt;/code&gt;, per RFC&lt;/p&gt;
&lt;h1&gt;
  
  
  0041. But it's not magic: if there's no active scope, the callback is never
&lt;/h1&gt;

&lt;p&gt;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&lt;br&gt;
"drift" means, but they know your clock looks wrong when it doesn't match&lt;br&gt;
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.&lt;br&gt;
A recursive &lt;code&gt;setTimeout&lt;/code&gt; that recalculates alignment on every tick ensures&lt;br&gt;
delay doesn't accumulate — the next tick always targets the next real clock&lt;br&gt;
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;,&lt;br&gt;
wrap it in &lt;code&gt;typeof window !== 'undefined'&lt;/code&gt;. And if you use Temporal, make&lt;br&gt;
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;br&gt;
&lt;code&gt;shallowRef&lt;/code&gt; avoids unnecessary proxy conversion and is the appropriate&lt;br&gt;
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&lt;br&gt;
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>
