<?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: ReactUse</title>
    <description>The latest articles on DEV Community by ReactUse (@childrentime).</description>
    <link>https://dev.to/childrentime</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%2F1119566%2F5db0ed4b-c605-4077-8ff4-34414f6b7257.png</url>
      <title>DEV Community: ReactUse</title>
      <link>https://dev.to/childrentime</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/childrentime"/>
    <language>en</language>
    <item>
      <title>React useTimeout Hook: Declarative setTimeout with Cleanup (2026)</title>
      <dc:creator>ReactUse</dc:creator>
      <pubDate>Fri, 21 Aug 2026 02:41:25 +0000</pubDate>
      <link>https://dev.to/childrentime/react-usetimeout-hook-declarative-settimeout-with-cleanup-2026-47pk</link>
      <guid>https://dev.to/childrentime/react-usetimeout-hook-declarative-settimeout-with-cleanup-2026-47pk</guid>
      <description>&lt;p&gt;Here is a "Copied!" button. Every codebase has one, and this version has three bugs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;CopyButton&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;copied&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCopied&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="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;copied&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="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="nf"&gt;setCopied&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2000&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="nx"&gt;copied&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="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="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nf"&gt;setCopied&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;copied&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Copied!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Copy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It never clears the timer, so unmounting mid-countdown leaves a callback scheduled against a dead component. It re-arms on every &lt;code&gt;copied&lt;/code&gt; change instead of restarting cleanly. And in React 18's StrictMode the effect runs twice on mount, so you get two timers where you meant one. Add the missing &lt;code&gt;clearTimeout&lt;/code&gt; and you've fixed the leak but not the shape of the problem: the timer's lifetime is now tangled into a dependency array, and there is still no way to cancel it from a click handler, restart it on demand, or ask "is it still running?"&lt;/p&gt;

&lt;p&gt;&lt;code&gt;setTimeout&lt;/code&gt; is a fire-and-forget browser primitive. React components are not fire-and-forget — they unmount, re-render, and change their minds. &lt;a href="https://reactuse.com/effect/usetimeout/" rel="noopener noreferrer"&gt;&lt;code&gt;useTimeout&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://reactuse.com/effect/usetimeoutfn/" rel="noopener noreferrer"&gt;&lt;code&gt;useTimeoutFn&lt;/code&gt;&lt;/a&gt; from &lt;a href="https://reactuse.com" rel="noopener noreferrer"&gt;&lt;code&gt;@reactuses/core&lt;/code&gt;&lt;/a&gt; close that gap by handing you the timer as a piece of state plus two controls, instead of a number you have to babysit. This post covers what they actually do under the hood, the one behavior that trips everyone up (the delay is a dependency, the callback isn't), a &lt;code&gt;start()&lt;/code&gt; trap that silently corrupts your arguments, and the patterns worth copying.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @reactuses/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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;useTimeoutFn&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@reactuses/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;CopyButton&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;copied&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCopied&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[,&lt;/span&gt; &lt;span class="nx"&gt;startReset&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useTimeoutFn&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setCopied&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;immediate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;
      &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="k"&gt;async &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;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clipboard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;setCopied&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="nf"&gt;startReset&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;copied&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Copied!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Copy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No effect, no dependency array, no cleanup to remember. The timer is armed by a click rather than by a render, it's cleared automatically on unmount, and clicking Copy again while the badge is still showing restarts the two seconds instead of stacking a second timer on top of the first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Hooks, One Engine
&lt;/h2&gt;

&lt;p&gt;Both hooks return the same three-element tuple — the library calls it &lt;code&gt;Stoppable&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Stoppable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isPending&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="nx"&gt;start&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;cancel&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They differ only in what happens at the deadline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://reactuse.com/effect/usetimeoutfn/" rel="noopener noreferrer"&gt;&lt;code&gt;useTimeoutFn(cb, ms, options?)&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; runs your callback. Use it when the deadline has a job to do — dismiss the toast, reset the flag, fire the analytics ping.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://reactuse.com/effect/usetimeout/" rel="noopener noreferrer"&gt;&lt;code&gt;useTimeout(ms?, options?)&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; runs no callback of yours. It flips &lt;code&gt;isPending&lt;/code&gt; from &lt;code&gt;true&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt; and re-renders. Use it when the deadline &lt;em&gt;is&lt;/em&gt; the state — "has 300ms passed yet?" is the whole question.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;useTimeout&lt;/code&gt; is literally &lt;code&gt;useTimeoutFn&lt;/code&gt; with the callback slot spent on a forced re-render:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UseTimeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;update&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useUpdate&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;useTimeoutFn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&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;That &lt;a href="https://reactuse.com/effect/useupdate/" rel="noopener noreferrer"&gt;&lt;code&gt;useUpdate&lt;/code&gt;&lt;/a&gt; is a two-line &lt;code&gt;useReducer&lt;/code&gt; that increments a counter modulo a million — the standard "force a re-render without inventing fake state" trick, with the modulo there so a long-lived component can't drift toward &lt;code&gt;Number.MAX_SAFE_INTEGER&lt;/code&gt;. It guarantees a render at the deadline even in the cases where &lt;code&gt;isPending&lt;/code&gt; alone wouldn't produce one, which is what lets you use &lt;code&gt;useTimeout&lt;/code&gt; as a bare "re-render me in N milliseconds" primitive when you need to re-read something that isn't React state.&lt;/p&gt;

&lt;p&gt;By default both start on mount. Pass &lt;code&gt;{ immediate: false }&lt;/code&gt; and nothing happens until you call &lt;code&gt;start()&lt;/code&gt; yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Actually Does
&lt;/h2&gt;

&lt;p&gt;The implementation is about twenty lines, and every one of them is answering a bug from the opening example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useTimeoutFn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cb&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="nx"&gt;options&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;immediate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setPending&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;immediate&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;savedCallback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useLatest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cb&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;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;ReturnType&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;setTimeout&lt;/span&gt;&lt;span class="o"&gt;&amp;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="nx"&gt;stop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useEvent&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;setPending&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="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="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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;current&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="nf"&gt;useEvent&lt;/span&gt;&lt;span class="p"&gt;((...&lt;/span&gt;&lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&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;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;current&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;current&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="nf"&gt;setPending&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;savedCallback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&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="nf"&gt;setPending&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;immediate&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;stop&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="nx"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;immediate&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="nx"&gt;start&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;pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;start&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five decisions are packed in there, and each one is worth knowing about because each one shows up in your code later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The callback lives in a ref, not in the deps.&lt;/strong&gt; &lt;a href="https://reactuse.com/state/uselatest/" rel="noopener noreferrer"&gt;&lt;code&gt;useLatest&lt;/code&gt;&lt;/a&gt; keeps &lt;code&gt;savedCallback.current&lt;/code&gt; pointing at the newest function after every committed render, and the timer calls through it. So the closure that fires at the deadline is the one from your &lt;em&gt;most recent&lt;/em&gt; render — the stale-closure bug is gone — but swapping the callback does &lt;strong&gt;not&lt;/strong&gt; restart the countdown. A timer started 4 seconds into a 5-second delay still has 1 second left, even if the function it will call has been re-created ten times since. That's the correct behavior and it's covered by a test in the repo, but it surprises people who expect a &lt;code&gt;useEffect&lt;/code&gt;-shaped hook to re-run when its inputs change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The delay &lt;em&gt;is&lt;/em&gt; in the deps.&lt;/strong&gt; &lt;code&gt;interval&lt;/code&gt; sits in the dependency array, so changing it tears the timer down and starts a fresh one from zero. Deliberate, and usually what you want — but see the gotchas, because a delay computed inline is an easy way to build a countdown that never finishes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;start&lt;/code&gt; and &lt;code&gt;stop&lt;/code&gt; never change identity.&lt;/strong&gt; &lt;a href="https://reactuse.com/effect/useevent/" rel="noopener noreferrer"&gt;&lt;code&gt;useEvent&lt;/code&gt;&lt;/a&gt; wraps both in a &lt;code&gt;useCallback&lt;/code&gt; with an empty dependency array that forwards to a ref, so the functions you get on render 1 are reference-identical to the ones on render 500. You can put them in dependency arrays, hand them to memoized children, or stash them in a context without any of the usual churn.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;start()&lt;/code&gt; clears before it sets.&lt;/strong&gt; Calling it while a timer is already running doesn't stack — it cancels and restarts. That's what makes "click Copy again" behave sanely, and it means repeatedly calling &lt;code&gt;start()&lt;/code&gt; on every keystroke gives you debounce semantics for free (though &lt;a href="https://reactuse.com/effect/usedebouncefn/" rel="noopener noreferrer"&gt;&lt;code&gt;useDebounceFn&lt;/code&gt;&lt;/a&gt; says what you mean more clearly).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;pending&lt;/code&gt; is seeded, not flashed.&lt;/strong&gt; &lt;code&gt;useState(() =&amp;gt; immediate)&lt;/code&gt; means that when &lt;code&gt;immediate&lt;/code&gt; is on, the very first render already reads &lt;code&gt;true&lt;/code&gt; — no &lt;code&gt;false → true&lt;/code&gt; flicker on mount, no wasted render. And because &lt;code&gt;immediate&lt;/code&gt; is a plain option with the same value on the server and the client, the seeded value is identical on both sides. Nothing in this hook touches &lt;code&gt;window&lt;/code&gt;, &lt;code&gt;document&lt;/code&gt; or &lt;code&gt;Date&lt;/code&gt;, so it renders on the server without a guard and hydrates without a mismatch.&lt;/p&gt;

&lt;p&gt;The effect's cleanup is &lt;code&gt;stop&lt;/code&gt; itself, which is the leak fix: unmount clears the timer, always, whatever state it was in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Patterns Worth Copying
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The delayed spinner
&lt;/h3&gt;

&lt;p&gt;The single best use of &lt;code&gt;useTimeout&lt;/code&gt;. A spinner that appears for 80ms and vanishes reads as a flicker — worse than no spinner at all. The fix is to only show it if loading is actually slow, which is exactly "has 300ms passed?":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;UserList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useUsers&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tooSoon&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;300&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;isLoading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;tooSoon&lt;/span&gt; &lt;span class="p"&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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Spinner&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tooSoon&lt;/code&gt; starts &lt;code&gt;true&lt;/code&gt; and flips to &lt;code&gt;false&lt;/code&gt; 300ms after mount. Fast responses render nothing at all in the gap; slow ones get a spinner. One line, no state, no effect.&lt;/p&gt;

&lt;h3&gt;
  
  
  Auto-dismiss with hover-to-pause
&lt;/h3&gt;

&lt;p&gt;The tuple's &lt;code&gt;cancel&lt;/code&gt; and &lt;code&gt;start&lt;/code&gt; are what make this trivial — a hand-rolled version needs a ref and two effects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Toast&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onDismiss&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;onDismiss&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[,&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useTimeoutFn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onDismiss&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5000&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt; &lt;span class="na"&gt;onMouseEnter&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cancel&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onMouseLeave&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&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="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;() =&amp;gt; start()&lt;/code&gt; on &lt;code&gt;onMouseLeave&lt;/code&gt;. That is not a style choice — see the gotchas.&lt;/p&gt;

&lt;h3&gt;
  
  
  A cooldown button
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ResendCodeButton&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;onResend&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;onResend&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;cooling&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;startCooldown&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;immediate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;
      &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cooling&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="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;onResend&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="nf"&gt;startCooldown&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cooling&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Code sent — try again shortly&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Resend code&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;immediate: false&lt;/code&gt; is the important part: the button is live on mount and only goes cold once it's been used. If you want to render the remaining seconds rather than a boolean, that's a different hook — &lt;a href="https://reactuse.com/state/usecountdown/" rel="noopener noreferrer"&gt;&lt;code&gt;useCountDown&lt;/code&gt;&lt;/a&gt; ticks down and hands you the number.&lt;/p&gt;

&lt;h3&gt;
  
  
  Yielding to the browser
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;useTimeout()&lt;/code&gt; with no arguments defaults to &lt;code&gt;ms = 0&lt;/code&gt;, which still defers to a macrotask — after paint, after pending microtasks. Occasionally that's exactly the escape hatch you want for "let the browser draw this frame before I do the expensive thing," and it's cheaper to reason about than a &lt;code&gt;requestIdleCallback&lt;/code&gt; polyfill. For anything that should run per-frame instead of once, use &lt;a href="https://reactuse.com/effect/useraffn/" rel="noopener noreferrer"&gt;&lt;code&gt;useRafFn&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas Worth Knowing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;start&lt;/code&gt; forwards its arguments to your callback.&lt;/strong&gt; This is a real feature — &lt;code&gt;start(userId)&lt;/code&gt; passes &lt;code&gt;userId&lt;/code&gt; through to the timer callback — and a real trap when the caller is a DOM handler. &lt;code&gt;onMouseLeave={start}&lt;/code&gt; hands React's synthetic &lt;code&gt;MouseEvent&lt;/code&gt; straight into your &lt;code&gt;onDismiss(...)&lt;/code&gt;. If that callback is &lt;code&gt;onDismiss(id?: string)&lt;/code&gt;, you've just dismissed a toast with an event object as its id, and TypeScript won't stop you because &lt;code&gt;start&lt;/code&gt; is typed as &lt;code&gt;Fn&lt;/code&gt;. Wrap it: &lt;code&gt;onMouseLeave={() =&amp;gt; start()}&lt;/code&gt;. Same rule for &lt;code&gt;onClick&lt;/code&gt;, &lt;code&gt;onBlur&lt;/code&gt;, and anything else that supplies an event.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A changing delay restarts the countdown — every time.&lt;/strong&gt; &lt;code&gt;interval&lt;/code&gt; is a dependency, so this never fires:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// BROKEN: a new delay on every render restarts the timer forever&lt;/span&gt;
  &lt;span class="nf"&gt;useTimeoutFn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;onDone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&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="nx"&gt;deadline&lt;/span&gt; &lt;span class="o"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any delay recomputed per render resets the clock before it can run out. Pass a stable number, or memoize it. The flip side is useful: when the delay genuinely changes — a user picking "dismiss after 3s / 10s / never" — the restart is exactly right.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A changing callback does &lt;em&gt;not&lt;/em&gt; restart it.&lt;/strong&gt; The mirror image, and equally worth internalizing. Your callback is always the latest one, but its scheduled deadline is whatever it was when &lt;code&gt;start()&lt;/code&gt; ran.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;cancel()&lt;/code&gt; sets &lt;code&gt;isPending&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt;.&lt;/strong&gt; It's a stop, not a pause — there is no "resume with the remaining time." &lt;code&gt;start()&lt;/code&gt; after &lt;code&gt;cancel()&lt;/code&gt; begins a fresh full delay. If you need true pause/resume semantics, track the elapsed time yourself and pass the remainder as the new delay.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;After unmount, &lt;code&gt;isPending&lt;/code&gt; freezes at its last rendered value.&lt;/strong&gt; The cleanup calls &lt;code&gt;stop()&lt;/code&gt;, which clears the timer and calls &lt;code&gt;setPending(false)&lt;/code&gt; — but that state update lands on an unmounted component, so React discards it. If you snapshot the tuple in a test and read it after &lt;code&gt;unmount()&lt;/code&gt;, &lt;code&gt;isPending&lt;/code&gt; will still be &lt;code&gt;true&lt;/code&gt;. It is not a leak and not a warning; the timer really is cleared.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;StrictMode double-arms, then converges.&lt;/strong&gt; In React 18 development the mount effect runs, cleans up, and runs again, so you'll see two &lt;code&gt;setTimeout&lt;/code&gt; calls in dev. There's never a duplicate firing — &lt;code&gt;stop&lt;/code&gt; clears the first one and &lt;code&gt;start&lt;/code&gt; clears again before scheduling — but the countdown effectively begins on the second run. In practice that's a sub-millisecond difference; in a test with fake timers advanced by exact amounts, it's a difference that can bite.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;immediate&lt;/code&gt; is read on mount and as a dependency.&lt;/strong&gt; Flipping &lt;code&gt;immediate&lt;/code&gt; from &lt;code&gt;false&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; on a later render &lt;em&gt;will&lt;/em&gt; start the timer, because it's in the effect's deps. Toggling it is a legitimate way to arm a timer declaratively — just don't be surprised when it's not inert.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When Not to Use It
&lt;/h2&gt;

&lt;p&gt;These hooks are a thin, honest wrapper over one &lt;code&gt;setTimeout&lt;/code&gt;. When your problem has a name, the named hook handles edge cases you'd otherwise rediscover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repeating on a schedule&lt;/strong&gt; → &lt;a href="https://reactuse.com/effect/useinterval/" rel="noopener noreferrer"&gt;&lt;code&gt;useInterval&lt;/code&gt;&lt;/a&gt;, not a timeout that re-arms itself. Self-rescheduling timeouts drift and are miserable to cancel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Wait until the user stops typing"&lt;/strong&gt; → &lt;a href="https://reactuse.com/effect/usedebouncefn/" rel="noopener noreferrer"&gt;&lt;code&gt;useDebounceFn&lt;/code&gt;&lt;/a&gt; for a callback, &lt;a href="https://reactuse.com/state/usedebounce/" rel="noopener noreferrer"&gt;&lt;code&gt;useDebounce&lt;/code&gt;&lt;/a&gt; for a value. You &lt;em&gt;can&lt;/em&gt; build this by calling &lt;code&gt;start()&lt;/code&gt; on each keystroke, but the dedicated hooks say so at a glance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"At most once every N ms"&lt;/strong&gt; → &lt;a href="https://reactuse.com/effect/usethrottlefn/" rel="noopener noreferrer"&gt;&lt;code&gt;useThrottleFn&lt;/code&gt;&lt;/a&gt; / &lt;a href="https://reactuse.com/state/usethrottle/" rel="noopener noreferrer"&gt;&lt;code&gt;useThrottle&lt;/code&gt;&lt;/a&gt;. A timeout is the wrong primitive for rate limiting; the first call should go through immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A visible countdown&lt;/strong&gt; → &lt;a href="https://reactuse.com/state/usecountdown/" rel="noopener noreferrer"&gt;&lt;code&gt;useCountDown&lt;/code&gt;&lt;/a&gt;. Rendering "4… 3… 2…" from a single timeout means running your own tick loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Has the user gone quiet?"&lt;/strong&gt; → &lt;a href="https://reactuse.com/browser/useidle/" rel="noopener noreferrer"&gt;&lt;code&gt;useIdle&lt;/code&gt;&lt;/a&gt;, which already watches the right set of activity events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-frame animation&lt;/strong&gt; → &lt;a href="https://reactuse.com/effect/useraffn/" rel="noopener noreferrer"&gt;&lt;code&gt;useRafFn&lt;/code&gt;&lt;/a&gt;. &lt;code&gt;setTimeout&lt;/code&gt; doesn't align to the compositor and keeps running in background tabs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cleanup on unmount only&lt;/strong&gt; → &lt;a href="https://reactuse.com/effect/useunmount/" rel="noopener noreferrer"&gt;&lt;code&gt;useUnmount&lt;/code&gt;&lt;/a&gt;. No timer needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;setTimeout&lt;/code&gt; in a &lt;code&gt;useEffect&lt;/code&gt; forces you to hand-manage four things at once: the cleanup, the dependency array, the stale closure, and the lack of controls. Getting three right and one wrong is the normal outcome.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://reactuse.com/effect/usetimeoutfn/" rel="noopener noreferrer"&gt;&lt;code&gt;useTimeoutFn&lt;/code&gt;&lt;/a&gt; returns &lt;code&gt;[isPending, start, cancel]&lt;/code&gt; and clears on unmount by construction. &lt;a href="https://reactuse.com/effect/usetimeout/" rel="noopener noreferrer"&gt;&lt;code&gt;useTimeout&lt;/code&gt;&lt;/a&gt; is the same engine with the callback spent on a re-render, for when the deadline itself is the state you care about.&lt;/li&gt;
&lt;li&gt;The delay is a dependency and the callback isn't — changing the delay restarts the countdown, changing the callback silently swaps what fires. Both are deliberate; knowing which is which saves an afternoon.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;start&lt;/code&gt; forwards its arguments, so never pass it directly to a DOM event handler. &lt;code&gt;onMouseLeave={() =&amp;gt; start()}&lt;/code&gt;, not &lt;code&gt;onMouseLeave={start}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;start&lt;/code&gt; and &lt;code&gt;cancel&lt;/code&gt; are identity-stable forever, &lt;code&gt;isPending&lt;/code&gt; is seeded so it doesn't flash on mount, and nothing in the hook touches a browser global — it renders on the server untouched.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;useTimeout&lt;/code&gt;, &lt;code&gt;useTimeoutFn&lt;/code&gt;, &lt;code&gt;useInterval&lt;/code&gt;, and 110+ other SSR-safe, TypeScript-first hooks live in &lt;a href="https://reactuse.com" rel="noopener noreferrer"&gt;&lt;code&gt;@reactuses/core&lt;/code&gt;&lt;/a&gt; — one install, tree-shakeable, no dependencies to babysit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @reactuses/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>React useEventListener Hook: Type-Safe DOM Events (2026)</title>
      <dc:creator>ReactUse</dc:creator>
      <pubDate>Thu, 20 Aug 2026 09:23:19 +0000</pubDate>
      <link>https://dev.to/childrentime/react-useeventlistener-hook-type-safe-dom-events-2026-5cfi</link>
      <guid>https://dev.to/childrentime/react-useeventlistener-hook-type-safe-dom-events-2026-5cfi</guid>
      <description>&lt;p&gt;Here's a modal close-on-Escape that quietly does the wrong thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Modal&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;onClose&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;onClose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;KeyboardEvent&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Escape&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;onClose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;keydown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;window&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="s2"&gt;keydown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onKey&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="nx"&gt;onClose&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"dialog"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;…&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the parent passes an inline &lt;code&gt;onClose={() =&amp;gt; setOpen(false)}&lt;/code&gt; — and it almost always does — &lt;code&gt;onClose&lt;/code&gt; is a new function on every render, so this effect tears the listener down and adds a fresh one on &lt;em&gt;every single render&lt;/em&gt; of the parent. Drop &lt;code&gt;onClose&lt;/code&gt; from the deps to stop the churn and you get the other bug: the listener now holds the first render's &lt;code&gt;onClose&lt;/code&gt; forever, and closing the modal calls a stale closure.&lt;/p&gt;

&lt;p&gt;You can't win this with a dependency array, because the two things you want are in direct conflict: &lt;strong&gt;subscribe once&lt;/strong&gt;, but &lt;strong&gt;always run the newest handler&lt;/strong&gt;. The fix is to separate them — register the listener on a stable identity, and call through a ref that's kept current. &lt;a href="https://reactuse.com/effect/useeventlistener/" rel="noopener noreferrer"&gt;&lt;code&gt;useEventListener&lt;/code&gt;&lt;/a&gt; from &lt;a href="https://reactuse.com" rel="noopener noreferrer"&gt;&lt;code&gt;@reactuses/core&lt;/code&gt;&lt;/a&gt; is that split, packaged. This post covers what it actually does under the hood, the four ways to name a target, exactly what TypeScript infers for each one (this part surprises people), the options that don't retrigger, and the two gotchas worth knowing before you ship it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @reactuses/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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;useEventListener&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@reactuses/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Modal&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;onClose&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;onClose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;useEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;keydown&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="nx"&gt;e&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Escape&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;onClose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"dialog"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;…&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole fix. No dependency array, no &lt;code&gt;useCallback&lt;/code&gt; on the parent, no cleanup to remember. The listener is added to &lt;code&gt;window&lt;/code&gt; once when the component mounts and removed when it unmounts; the arrow function you passed is re-created on every render and it doesn't matter, because the listener never re-registers — it calls the latest one. &lt;code&gt;e&lt;/code&gt; is a &lt;code&gt;KeyboardEvent&lt;/code&gt;, inferred, not annotated.&lt;/p&gt;

&lt;p&gt;The signature is four arguments, three of them optional:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;target&lt;/code&gt; defaults to &lt;code&gt;window&lt;/code&gt;. &lt;code&gt;options&lt;/code&gt; is the same &lt;code&gt;boolean | AddEventListenerOptions&lt;/code&gt; you'd pass to &lt;code&gt;addEventListener&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Actually Does
&lt;/h2&gt;

&lt;p&gt;The implementation is short enough to read in full, and worth reading because every line is answering one of the problems above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eventName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;savedHandler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useLatest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handler&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;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;elementKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;elementRef&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useStableTarget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;defaultWindow&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useDeepCompareEffect&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="nx"&gt;targetElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getTargetElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;elementRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;defaultWindow&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetElement&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;targetElement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addEventListener&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;eventListener&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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;savedHandler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;eventName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;eventListener&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;off&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;eventName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;eventListener&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="nx"&gt;eventName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;elementKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&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;Four decisions are packed in there:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The handler is held in a ref, not in the deps.&lt;/strong&gt; &lt;a href="https://reactuse.com/state/uselatest/" rel="noopener noreferrer"&gt;&lt;code&gt;useLatest&lt;/code&gt;&lt;/a&gt; keeps &lt;code&gt;savedHandler.current&lt;/code&gt; pointing at the newest handler after every committed render, and the function actually registered with the DOM is a thin wrapper that forwards to it. So the handler you pass can be a brand-new closure every render — inline arrow functions are not just allowed, they're the expected usage — while &lt;code&gt;addEventListener&lt;/code&gt; is called exactly once. That's the "subscribe once, run the newest" split, and it's why &lt;code&gt;handler&lt;/code&gt; is deliberately absent from the dependency list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dependency list is deep-compared.&lt;/strong&gt; The effect is &lt;a href="https://reactuse.com/effect/usedeepcompareeffect/" rel="noopener noreferrer"&gt;&lt;code&gt;useDeepCompareEffect&lt;/code&gt;&lt;/a&gt;, not &lt;code&gt;useEffect&lt;/code&gt;, so a fresh-but-identical &lt;code&gt;options&lt;/code&gt; object each render doesn't count as a change. Writing &lt;code&gt;useEventListener("scroll", onScroll, ref, { passive: true })&lt;/code&gt; with the object literal inline is fine: three renders, one &lt;code&gt;addEventListener&lt;/code&gt; call. Change the contents to &lt;code&gt;{ passive: false }&lt;/code&gt; and it does re-register, which is what you want.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The target is resolved inside the effect, at commit time.&lt;/strong&gt; &lt;code&gt;getTargetElement&lt;/code&gt; runs in the effect body rather than during render, so a ref target has already been populated by React — &lt;code&gt;ref.current&lt;/code&gt; is &lt;code&gt;null&lt;/code&gt; while rendering and only becomes a node in the commit phase. This is the difference between the listener attaching and silently doing nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On the server it's a no-op.&lt;/strong&gt; The export is &lt;code&gt;isBrowser ? implementation : noop&lt;/code&gt;, so nothing touches &lt;code&gt;window&lt;/code&gt; during SSR and there's no &lt;code&gt;typeof window === "undefined"&lt;/code&gt; guard for you to write. Listeners attach after hydration, in the effect, like every other browser subscription.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Four Ways to Name a Target
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;target&lt;/code&gt; accepts four shapes, and picking the right one is most of the API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 1. omitted → window&lt;/span&gt;
&lt;span class="nf"&gt;useEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;resize&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setWidth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerWidth&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// 2. a function returning an element → document, or anything you look up lazily&lt;/span&gt;
&lt;span class="nf"&gt;useEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;visibilitychange&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setActive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// 3. a ref&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;boxRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLDivElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;useEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wheel&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="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WheelEvent&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;boxRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;passive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// 4. any EventTarget you already hold&lt;/span&gt;
&lt;span class="nf"&gt;useEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;message&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="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MessageEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Case 2 exists because &lt;code&gt;document&lt;/code&gt; isn't available during module evaluation on the server, and because passing &lt;code&gt;document&lt;/code&gt; directly would be a new-identity-every-render problem for anything looked up on the fly. The wrapper function is resolved at commit time and its &lt;em&gt;result&lt;/em&gt; is what the effect keys on, so &lt;code&gt;() =&amp;gt; document&lt;/code&gt; is stable in the way that matters.&lt;/p&gt;

&lt;p&gt;Case 4 is the one people forget: &lt;code&gt;EventTarget&lt;/code&gt; is not just DOM elements. A &lt;code&gt;Worker&lt;/code&gt;, a &lt;code&gt;WebSocket&lt;/code&gt;, an &lt;code&gt;EventSource&lt;/code&gt;, a &lt;code&gt;MediaQueryList&lt;/code&gt;, &lt;code&gt;window.visualViewport&lt;/code&gt;, a &lt;code&gt;BroadcastChannel&lt;/code&gt;, an &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; element, &lt;code&gt;navigator.serviceWorker&lt;/code&gt;, even an &lt;code&gt;AbortSignal&lt;/code&gt; — all of them are event targets, and all of them work here with the same automatic cleanup. (For the common ones, the library already ships purpose-built hooks: &lt;a href="https://reactuse.com/browser/useeventsource/" rel="noopener noreferrer"&gt;&lt;code&gt;useEventSource&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://reactuse.com/browser/usebroadcastchannel/" rel="noopener noreferrer"&gt;&lt;code&gt;useBroadcastChannel&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://reactuse.com/browser/usemediaquery/" rel="noopener noreferrer"&gt;&lt;code&gt;useMediaQuery&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://reactuse.com/browser/usenetwork/" rel="noopener noreferrer"&gt;&lt;code&gt;useNetwork&lt;/code&gt;&lt;/a&gt;.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What TypeScript Actually Infers
&lt;/h2&gt;

&lt;p&gt;This is the part worth being precise about, because the hook ships six overloads and they don't all give you the same thing. Verified against the current types:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target form&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;e&lt;/code&gt; is inferred as&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;omitted (&lt;code&gt;window&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;the exact &lt;code&gt;WindowEventMap&lt;/code&gt; type — &lt;code&gt;"keydown"&lt;/code&gt; → &lt;code&gt;KeyboardEvent&lt;/code&gt; ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a raw &lt;code&gt;HTMLElement&lt;/code&gt; / &lt;code&gt;Element&lt;/code&gt; / &lt;code&gt;Document&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;the exact event type — &lt;code&gt;"click"&lt;/code&gt; → &lt;code&gt;MouseEvent&lt;/code&gt; ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a &lt;strong&gt;ref object&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;any&lt;/code&gt; ⚠️&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a &lt;strong&gt;function&lt;/strong&gt; target (&lt;code&gt;() =&amp;gt; document&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;any&lt;/code&gt; ⚠️&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two &lt;code&gt;any&lt;/code&gt; cases fall through to the general overload, which types the handler as &lt;code&gt;(...p: any) =&amp;gt; void&lt;/code&gt;. Nothing breaks — but you lose the autocomplete and the type checking exactly where refs are most common. The fix is one annotation, and it costs nothing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ⚠️ e is any&lt;/span&gt;
&lt;span class="nf"&gt;useEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&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="nx"&gt;e&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;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="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientX&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;buttonRef&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ e is MouseEvent, checked&lt;/span&gt;
&lt;span class="nf"&gt;useEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&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="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MouseEvent&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;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="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientX&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;buttonRef&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two related sharp edges in the same area. First, &lt;code&gt;e&lt;/code&gt; is the &lt;strong&gt;native&lt;/strong&gt; DOM event, not React's &lt;code&gt;SyntheticEvent&lt;/code&gt; — &lt;code&gt;e.target&lt;/code&gt; is not typed for you, &lt;code&gt;e.currentTarget&lt;/code&gt; is &lt;code&gt;EventTarget | null&lt;/code&gt;, and there's no pooling to worry about. Second, the event &lt;em&gt;name&lt;/em&gt; is only constrained when the target is one of the typed overloads; with a ref or function target the name is a plain &lt;code&gt;string&lt;/code&gt;, so a typo like &lt;code&gt;"keydwon"&lt;/code&gt; compiles happily and attaches a listener that never fires. If a listener seems dead, check the spelling before you check anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Keyboard shortcuts
&lt;/h3&gt;

&lt;p&gt;The canonical &lt;code&gt;window&lt;/code&gt; listener. One hook per shortcut, or one handler with a switch — both are fine, because neither re-registers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useShortcut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;combo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;KeyboardEvent&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;boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;useEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;keydown&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="nx"&gt;e&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="nf"&gt;combo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&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="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;CommandBar&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setOpen&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;useShortcut&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metaKey&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ctrlKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;k&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setOpen&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="nf"&gt;useShortcut&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Escape&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setOpen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="c1"&gt;// …&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the composition: &lt;code&gt;useEventListener&lt;/code&gt; is a fine primitive to build &lt;em&gt;your&lt;/em&gt; hooks on, and because the handler is ref-held, &lt;code&gt;run&lt;/code&gt; and &lt;code&gt;combo&lt;/code&gt; can be inline closures over fresh state without any memoization ceremony. If you only need the modifier keys themselves, &lt;a href="https://reactuse.com/browser/usekeymodifier/" rel="noopener noreferrer"&gt;&lt;code&gt;useKeyModifier&lt;/code&gt;&lt;/a&gt; already tracks them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Non-passive wheel and touch listeners
&lt;/h3&gt;

&lt;p&gt;This is the case JSX props genuinely cannot do. React attaches &lt;code&gt;onWheel&lt;/code&gt; and &lt;code&gt;onTouchStart&lt;/code&gt; as passive listeners at the root, so calling &lt;code&gt;e.preventDefault()&lt;/code&gt; inside them logs a console warning and does nothing. To actually block a scroll or a pinch you need a real listener registered with &lt;code&gt;{ passive: false }&lt;/code&gt; on the element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ZoomCanvas&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;canvasRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLDivElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;zoom&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setZoom&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;wheel&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="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WheelEvent&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ctrlKey&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// works — this listener is genuinely non-passive&lt;/span&gt;
      &lt;span class="nf"&gt;setZoom&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deltaY&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;canvasRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;passive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;canvasRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`scale(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;zoom&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mirror image is just as useful: mark a high-frequency &lt;code&gt;scroll&lt;/code&gt; or &lt;code&gt;touchmove&lt;/code&gt; listener &lt;code&gt;{ passive: true }&lt;/code&gt; so the browser knows it never needs to wait on your handler before scrolling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Window and document events React doesn't give you props for
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;resize&lt;/code&gt;, &lt;code&gt;online&lt;/code&gt;/&lt;code&gt;offline&lt;/code&gt;, &lt;code&gt;visibilitychange&lt;/code&gt;, &lt;code&gt;beforeunload&lt;/code&gt;, &lt;code&gt;hashchange&lt;/code&gt;, &lt;code&gt;storage&lt;/code&gt;, &lt;code&gt;paste&lt;/code&gt; at the document level — none of these have a JSX equivalent, and all of them are one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useOnlineStatus&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;online&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setOnline&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;useEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;online&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setOnline&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="nf"&gt;useEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;offline&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setOnline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;online&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;Before you write these by hand, check whether the library already has them — &lt;a href="https://reactuse.com/element/usewindowsize/" rel="noopener noreferrer"&gt;&lt;code&gt;useWindowSize&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://reactuse.com/browser/useonline/" rel="noopener noreferrer"&gt;&lt;code&gt;useOnline&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://reactuse.com/element/usedocumentvisibility/" rel="noopener noreferrer"&gt;&lt;code&gt;useDocumentVisibility&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://reactuse.com/browser/usepageleave/" rel="noopener noreferrer"&gt;&lt;code&gt;usePageLeave&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://reactuse.com/state/usetextselection/" rel="noopener noreferrer"&gt;&lt;code&gt;useTextSelection&lt;/code&gt;&lt;/a&gt; are all thin wrappers over exactly this hook, with the state management already done.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rate-limiting a hot event
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;scroll&lt;/code&gt;, &lt;code&gt;mousemove&lt;/code&gt;, &lt;code&gt;resize&lt;/code&gt; and &lt;code&gt;pointermove&lt;/code&gt; fire far faster than you want to re-render. Wrap the handler, not the listener:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ScrollSpy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setY&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&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;onScroll&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useThrottleFn&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollY&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;useEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scroll&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onScroll&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;progress&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&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;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollHeight&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://reactuse.com/effect/usethrottlefn/" rel="noopener noreferrer"&gt;&lt;code&gt;useThrottleFn&lt;/code&gt;&lt;/a&gt; for "at most every N ms", &lt;a href="https://reactuse.com/effect/usedebouncefn/" rel="noopener noreferrer"&gt;&lt;code&gt;useDebounceFn&lt;/code&gt;&lt;/a&gt; for "once the user stops". Both keep a stable identity, so the listener still registers once. For scroll position specifically, &lt;a href="https://reactuse.com/browser/usescroll/" rel="noopener noreferrer"&gt;&lt;code&gt;useScroll&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://reactuse.com/element/usewindowscroll/" rel="noopener noreferrer"&gt;&lt;code&gt;useWindowScroll&lt;/code&gt;&lt;/a&gt; already do this properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas Worth Knowing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A ref target keys on the ref, not on &lt;code&gt;ref.current&lt;/code&gt;.&lt;/strong&gt; The effect's dependency is the ref &lt;em&gt;object&lt;/em&gt;, which is stable for the component's lifetime, so if the DOM node behind the ref is replaced — a conditional branch that mounts a genuinely different element, a &lt;code&gt;key&lt;/code&gt; change, a list reorder — the listener stays attached to the old, detached node and never moves. React usually reuses the same DOM node when the element type and position match, which is why this rarely bites, but when it does it's baffling. The fix is to make the &lt;em&gt;node&lt;/em&gt; the dependency by holding it in state with a callback ref:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setNode&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLElement&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;useEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&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="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MouseEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;show&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;setNode&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;A&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;setNode&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;B&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the target identity changes when the node does, and the listener re-registers on the new element.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Listeners attach after paint, not during render.&lt;/strong&gt; It's an effect, so between first paint and the effect running there is a window — usually a frame — where the listener isn't there yet. Irrelevant for user-driven events (nobody presses a key that fast), but it means you cannot use this to catch an event that fires during mount. If an element needs a listener from its very first paint, that's what layout effects and JSX props are for.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Passing &lt;code&gt;document&lt;/code&gt; or an element directly is fine — until it's conditional.&lt;/strong&gt; &lt;code&gt;useEventListener("click", h, someState ? elA : elB)&lt;/code&gt; re-registers when the element changes, which is correct. But &lt;code&gt;useEventListener("click", h, document.getElementById("x"))&lt;/code&gt; runs a DOM query on every render and returns &lt;code&gt;null&lt;/code&gt; on the server; prefer the function form &lt;code&gt;() =&amp;gt; document.getElementById("x")&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It doesn't return an &lt;code&gt;off()&lt;/code&gt; handle.&lt;/strong&gt; Unlike VueUse's version, there's no manual stop function — the lifetime is the component's. If you need to start and stop a listener on demand, gate it inside the handler with a ref or a piece of state (&lt;code&gt;if (!enabledRef.current) return&lt;/code&gt;), which is cheaper than re-registering anyway.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;One hook, one event.&lt;/strong&gt; There's no array form. &lt;code&gt;useEventListener("mousedown", h)&lt;/code&gt; and &lt;code&gt;useEventListener("touchstart", h)&lt;/code&gt; as two calls is the idiom — hooks are cheap, and it keeps the dependency comparison trivial.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SSR-safe by construction, so don't guard it.&lt;/strong&gt; No &lt;code&gt;typeof window&lt;/code&gt; checks, no &lt;code&gt;useEffect&lt;/code&gt; wrapper, no dynamic import. On the server the hook does nothing at all.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When Not to Use It
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;useEventListener&lt;/code&gt; is a primitive. If a purpose-built hook exists, it will handle the state, the edge cases and the cleanup you'd otherwise re-derive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A click landing outside an element&lt;/strong&gt; → &lt;a href="https://reactuse.com/element/useclickoutside/" rel="noopener noreferrer"&gt;&lt;code&gt;useClickOutside&lt;/code&gt;&lt;/a&gt; or &lt;a href="https://reactuse.com/element/useclickaway/" rel="noopener noreferrer"&gt;&lt;code&gt;useClickAway&lt;/code&gt;&lt;/a&gt; (they handle the "mousedown started inside, mouseup outside" case you'd get wrong).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hover, long-press, double-click, drag&lt;/strong&gt; → &lt;a href="https://reactuse.com/state/usehover/" rel="noopener noreferrer"&gt;&lt;code&gt;useHover&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://reactuse.com/browser/uselongpress/" rel="noopener noreferrer"&gt;&lt;code&gt;useLongPress&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://reactuse.com/element/usedoubleclick/" rel="noopener noreferrer"&gt;&lt;code&gt;useDoubleClick&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://reactuse.com/element/usedraggable/" rel="noopener noreferrer"&gt;&lt;code&gt;useDraggable&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Element size or visibility&lt;/strong&gt; → &lt;a href="https://reactuse.com/element/useresizeobserver/" rel="noopener noreferrer"&gt;&lt;code&gt;useResizeObserver&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://reactuse.com/element/useelementsize/" rel="noopener noreferrer"&gt;&lt;code&gt;useElementSize&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://reactuse.com/element/useintersectionobserver/" rel="noopener noreferrer"&gt;&lt;code&gt;useIntersectionObserver&lt;/code&gt;&lt;/a&gt;. These are observers, not events; a &lt;code&gt;resize&lt;/code&gt; listener on &lt;code&gt;window&lt;/code&gt; cannot tell you an element changed size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The event has a JSX prop and the target is your own element&lt;/strong&gt; → just use &lt;code&gt;onClick&lt;/code&gt;. Delegated React handlers are cheaper and colocated. Reach for a real listener when you need &lt;code&gt;window&lt;/code&gt;/&lt;code&gt;document&lt;/code&gt;, a non-passive listener, or an event React doesn't surface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You only wanted a stable function identity&lt;/strong&gt; → that's &lt;a href="https://reactuse.com/effect/useevent/" rel="noopener noreferrer"&gt;&lt;code&gt;useEvent&lt;/code&gt;&lt;/a&gt;; no listener involved.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;useEffect&lt;/code&gt; + &lt;code&gt;addEventListener&lt;/code&gt; pair forces a false choice: put the handler in the deps and re-subscribe on every render, or leave it out and call a stale closure.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://reactuse.com/effect/useeventlistener/" rel="noopener noreferrer"&gt;&lt;code&gt;useEventListener&lt;/code&gt;&lt;/a&gt; resolves it by registering a stable wrapper once and forwarding to a &lt;a href="https://reactuse.com/state/uselatest/" rel="noopener noreferrer"&gt;&lt;code&gt;useLatest&lt;/code&gt;&lt;/a&gt; ref, so inline arrow handlers are free and &lt;code&gt;addEventListener&lt;/code&gt; runs once per target.&lt;/li&gt;
&lt;li&gt;Options are deep-compared, so an inline &lt;code&gt;{ passive: true }&lt;/code&gt; doesn't retrigger; the target is resolved at commit time, so refs work; the whole hook is a no-op during SSR.&lt;/li&gt;
&lt;li&gt;TypeScript infers the exact event type for &lt;code&gt;window&lt;/code&gt; and raw-element targets, and falls back to &lt;code&gt;any&lt;/code&gt; for ref and function targets — annotate the handler parameter there, and watch for event-name typos, which those overloads won't catch.&lt;/li&gt;
&lt;li&gt;Use it as a primitive to build your own hooks on. If a dedicated hook already exists for what you're listening to, use that instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;useEventListener&lt;/code&gt;, &lt;code&gt;useLatest&lt;/code&gt;, &lt;code&gt;useClickOutside&lt;/code&gt;, and 110+ other SSR-safe, TypeScript-first hooks live in &lt;a href="https://reactuse.com" rel="noopener noreferrer"&gt;&lt;code&gt;@reactuses/core&lt;/code&gt;&lt;/a&gt; — one install, tree-shakeable, no dependencies to babysit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @reactuses/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>React useScrollLock Hook: Lock Body Scroll for Modals (2026)</title>
      <dc:creator>ReactUse</dc:creator>
      <pubDate>Wed, 19 Aug 2026 09:04:23 +0000</pubDate>
      <link>https://dev.to/childrentime/react-usescrolllock-hook-lock-body-scroll-for-modals-2026-2ach</link>
      <guid>https://dev.to/childrentime/react-usescrolllock-hook-lock-body-scroll-for-modals-2026-2ach</guid>
      <description>&lt;p&gt;Your modal is open, centered, perfect. Then someone flicks the overlay and the page behind it scrolls away underneath. Everyone's first fix is the same three lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;overflow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;open&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;open&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 on your laptop. Then the bug reports arrive:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;On iPhone the page still moves.&lt;/strong&gt; iOS Safari rubber-band scrolls the document by touch even with &lt;code&gt;overflow: hidden&lt;/code&gt; on &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Something else got wiped.&lt;/strong&gt; &lt;code&gt;""&lt;/code&gt; isn't necessarily what was there before — you just erased whatever your design system or CSS-in-JS had set inline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two overlays, one frozen page.&lt;/strong&gt; A drawer and a lightbox both own &lt;code&gt;body.style.overflow&lt;/code&gt;; close them in the wrong order and the page never scrolls again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The layout jumps&lt;/strong&gt; the instant the desktop scrollbar disappears.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://reactuse.com/browser/usescrolllock/" rel="noopener noreferrer"&gt;&lt;code&gt;useScrollLock&lt;/code&gt;&lt;/a&gt; from &lt;a href="https://reactuse.com" rel="noopener noreferrer"&gt;&lt;code&gt;@reactuses/core&lt;/code&gt;&lt;/a&gt; is those three lines with the hard parts handled: it restores the exact inline &lt;code&gt;overflow&lt;/code&gt; it replaced, adds a &lt;code&gt;touchmove&lt;/code&gt; guard on iOS that still lets your modal's own content scroll, exposes the lock as React state you can render off, and works on any element — not just &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;. This post covers what it actually does line by line, why &lt;code&gt;overflow: hidden&lt;/code&gt; is not enough on iOS, how it compares to the &lt;code&gt;position: fixed&lt;/code&gt; and &lt;code&gt;body:has(dialog[open])&lt;/code&gt; approaches, and the six gotchas that show up in real apps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @reactuses/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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;useScrollLock&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@reactuses/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Modal&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onClose&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;ModalProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// a getter, not `document.body` — see the SSR gotcha below&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[,&lt;/span&gt; &lt;span class="nx"&gt;setLocked&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useScrollLock&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setLocked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setLocked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// release even if we unmount while open&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLocked&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"overlay"&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onClose&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"sheet"&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stopPropagation&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The signature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;locked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLocked&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useScrollLock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;target&lt;/code&gt;&lt;/strong&gt; — the element whose scrolling you're locking. Accepts an element, a &lt;code&gt;RefObject&lt;/code&gt;, or a getter &lt;code&gt;() =&amp;gt; element&lt;/code&gt;. Resolved lazily, on every call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;initialState&lt;/code&gt;&lt;/strong&gt; — start locked. Defaults to &lt;code&gt;false&lt;/code&gt;, and you should keep it that way (gotcha 3).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Returns&lt;/strong&gt; &lt;code&gt;[locked, setLocked]&lt;/code&gt;. &lt;code&gt;locked&lt;/code&gt; is real state; &lt;code&gt;setLocked&lt;/code&gt; is identity-stable, so it's safe in a dependency array or as a prop.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What useScrollLock Actually Does
&lt;/h2&gt;

&lt;p&gt;The core of it, condensed from the source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;locked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLocked&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initialState&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;initialOverflowRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CSSStyleDeclaration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;overflow&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scroll&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getTargetElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&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;element&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;initialOverflowRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// remember what we're replacing&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;locked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;overflow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hidden&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="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;locked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;target&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;lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useEvent&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="nx"&gt;element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getTargetElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;locked&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isIOS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;element&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="s2"&gt;touchmove&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;passive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;setLocked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unlock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useEvent&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="nx"&gt;element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getTargetElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;locked&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isIOS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;element&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="s2"&gt;touchmove&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;overflow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;initialOverflowRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// restore, don't clobber&lt;/span&gt;
  &lt;span class="nf"&gt;setLocked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four decisions in there are worth naming, because they're exactly where hand-rolled versions differ:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The lock is state, not a fire-and-forget side effect.&lt;/strong&gt; &lt;code&gt;locked&lt;/code&gt; is a real &lt;code&gt;useState&lt;/code&gt; value, so the same boolean that drives the style can drive your &lt;code&gt;aria-hidden&lt;/code&gt;, your class names, your Esc handler.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It restores the inline value it replaced&lt;/strong&gt;, not &lt;code&gt;""&lt;/code&gt;. If something had set &lt;code&gt;overflow: overlay&lt;/code&gt; inline, that's what comes back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The target is resolved lazily&lt;/strong&gt; through &lt;code&gt;getTargetElement&lt;/code&gt;, which returns &lt;code&gt;undefined&lt;/code&gt; when there is no &lt;code&gt;window&lt;/code&gt;. Nothing touches the DOM on the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only iOS gets a &lt;code&gt;touchmove&lt;/code&gt; guard.&lt;/strong&gt; Which is the genuinely interesting part.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;overflow: hidden&lt;/code&gt; Isn't Enough on iOS
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;overflow: hidden&lt;/code&gt; on the scrolling element is the correct, spec-blessed way to stop scrolling — and iOS Safari has never fully honored it on &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;. Touch drags still rubber-band the document. The only reliable stop is to cancel the gesture itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;element&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="s2"&gt;touchmove&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;passive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;passive: false&lt;/code&gt; is mandatory here, not decoration. Browsers register touch listeners on document-level targets as passive by default, and a passive listener's &lt;code&gt;preventDefault()&lt;/code&gt; is ignored with a console warning — your lock would silently do nothing.&lt;/p&gt;

&lt;p&gt;But a blanket &lt;code&gt;preventDefault&lt;/code&gt; on &lt;code&gt;touchmove&lt;/code&gt; breaks the thing you actually wanted: scrolling &lt;em&gt;inside&lt;/em&gt; the modal. So the handler asks a question before cancelling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkOverflowScroll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ele&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Element&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getComputedStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ele&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;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;overflowX&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scroll&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;overflowY&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scroll&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;overflowX&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto&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="nx"&gt;ele&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientWidth&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;ele&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollWidth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;overflowY&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto&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="nx"&gt;ele&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clientHeight&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;ele&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollHeight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&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;parent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ele&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentNode&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Element&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tagName&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BODY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&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;checkOverflowScroll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parent&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;Walk up from &lt;code&gt;event.target&lt;/code&gt;; if any ancestor is genuinely scrollable — &lt;code&gt;overflow: scroll&lt;/code&gt;, or &lt;code&gt;overflow: auto&lt;/code&gt; &lt;strong&gt;with content that actually overflows right now&lt;/strong&gt; — let the gesture through untouched. Otherwise cancel it. Two nice properties fall out of that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An &lt;code&gt;overflow: auto&lt;/code&gt; container whose content currently &lt;em&gt;fits&lt;/em&gt; is not scrollable, so it gets locked — correctly. Add enough content and it starts scrolling again with no code change.&lt;/li&gt;
&lt;li&gt;Multi-touch is excluded (&lt;code&gt;if (e.touches.length &amp;gt; 1) return true&lt;/code&gt;, before any &lt;code&gt;preventDefault&lt;/code&gt;), so pinch-to-zoom keeps working. Killing zoom inside a modal is an accessibility regression, and this sidesteps it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  useScrollLock vs the Other Four Approaches
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Stops iOS rubber-band&lt;/th&gt;
&lt;th&gt;Keeps inner scroll&lt;/th&gt;
&lt;th&gt;Keeps scroll position&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;body.style.overflow = "hidden"&lt;/code&gt; by hand&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;clobbers the inline style, never restores it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;body:has(dialog[open]) { overflow: hidden }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;zero JS — but the same iOS hole&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;body { position: fixed; top: -scrollY }&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;only if you save and restore it yourself&lt;/td&gt;
&lt;td&gt;takes &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; out of flow: &lt;code&gt;position: fixed&lt;/code&gt; children re-anchor, scroll anchoring and &lt;code&gt;scroll-behavior: smooth&lt;/code&gt; get strange&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;overscroll-behavior: contain&lt;/code&gt; on the dialog &lt;strong&gt;and&lt;/strong&gt; &lt;code&gt;::backdrop&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;✅ (Chrome 144+)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;cleanest of all, where it's supported — and only for &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;useScrollLock&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;~40 lines of JS behind one hook call&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One thing that trips people up: &lt;code&gt;&amp;lt;dialog&amp;gt;.showModal()&lt;/code&gt; makes the rest of the document &lt;strong&gt;inert&lt;/strong&gt; — clicks and Tab can't reach it — but it does &lt;em&gt;not&lt;/em&gt; reliably block scrolling, particularly by touch on mobile. Inertness and scroll-locking are separate problems, and the browser only solves the first one for you.&lt;/p&gt;

&lt;p&gt;And a complement rather than an alternative: &lt;code&gt;overscroll-behavior: contain&lt;/code&gt; on your &lt;em&gt;inner&lt;/em&gt; scroller stops scroll &lt;strong&gt;chaining&lt;/strong&gt; — the inner list hitting its end and handing the gesture to the page. That's worth adding regardless of how you lock, but on its own it doesn't stop a drag that started on the backdrop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Declare the lock, don't toggle it
&lt;/h3&gt;

&lt;p&gt;The Quick Start example is the pattern to internalize. Instead of calling &lt;code&gt;setLocked(true)&lt;/code&gt; in your open handler and &lt;code&gt;setLocked(false)&lt;/code&gt; in your close handler — two places to forget, plus every early-return path in between — bind the lock to the state that already describes the modal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setLocked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setLocked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLocked&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the lock can't drift out of sync with the UI, and the cleanup covers the case the imperative version always misses: a route change that unmounts the modal while it's open.&lt;/p&gt;

&lt;p&gt;Combined with &lt;a href="https://reactuse.com/state/usedisclosure/" rel="noopener noreferrer"&gt;&lt;code&gt;useDisclosure&lt;/code&gt;&lt;/a&gt; for the open/close state itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useDisclosure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useScrollLock&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@reactuses/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Drawer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;isOpen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onOpen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onClose&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useDisclosure&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;locked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLocked&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useScrollLock&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setLocked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setLocked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLocked&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="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onOpen&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Menu&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt; &lt;span class="na"&gt;aria-hidden&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;locked&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* page content */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;main&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;aside&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"drawer"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onClose&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Close&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;aside&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;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;Note the &lt;code&gt;locked&lt;/code&gt; half of the tuple earning its keep: one boolean drives both the style and the accessibility state, so they cannot disagree. (On React 19 you can use the same value for &lt;code&gt;inert&lt;/code&gt;.)&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Lock a scroll container, not the document
&lt;/h3&gt;

&lt;p&gt;Plenty of apps don't scroll the document at all — the shell is &lt;code&gt;height: 100vh; overflow: auto&lt;/code&gt; and everything scrolls inside a div. &lt;code&gt;overflow: hidden&lt;/code&gt; on &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; does exactly nothing there, which is a confusing afternoon if you don't know it. Point the hook at the real scroller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Shell&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&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;scroller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLDivElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[,&lt;/span&gt; &lt;span class="nx"&gt;setLocked&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useScrollLock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scroller&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;scroller&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;100vh&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;overflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same hook, same tuple. This is why &lt;code&gt;target&lt;/code&gt; is required rather than defaulting to &lt;code&gt;document.body&lt;/code&gt;: the library can't know which element is your scroll root.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Lock during a drag
&lt;/h3&gt;

&lt;p&gt;Touch-dragging a slider, a sortable list, or a custom carousel scrolls the page unless something stops it — and a &lt;code&gt;touchmove&lt;/code&gt; guard is exactly the right tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[,&lt;/span&gt; &lt;span class="nx"&gt;setLocked&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useScrollLock&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;
  &lt;span class="na"&gt;onPointerDown&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&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="nf"&gt;setLocked&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="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onPointerUp&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&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="nf"&gt;setLocked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;onPointerCancel&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&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="nf"&gt;setLocked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;onPointerCancel&lt;/code&gt; matters: the browser can steal the pointer mid-gesture, and without it you'd leave the page locked. If you're building the drag itself rather than wiring one up, &lt;a href="https://reactuse.com/element/usedraggable/" rel="noopener noreferrer"&gt;&lt;code&gt;useDraggable&lt;/code&gt;&lt;/a&gt; already handles the pointer bookkeeping.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas Worth Knowing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The lock is a style, not a lifecycle
&lt;/h3&gt;

&lt;p&gt;The lock is an inline &lt;code&gt;overflow: hidden&lt;/code&gt; written onto an element the hook doesn't own, so something has to put it back. Since &lt;code&gt;@reactuses/core&lt;/code&gt; v6.5.3 the hook does that itself when the owning component unmounts: it restores the exact inline value it replaced and detaches the iOS &lt;code&gt;touchmove&lt;/code&gt; guard, so a route change with the modal still open can no longer leave the page frozen. On v6.5.2 and earlier it didn't — worth knowing if you're pinned to an older version, because on iOS the leftover &lt;code&gt;passive: false&lt;/code&gt; listener kills touch scrolling for the rest of the session, not just the style.&lt;/p&gt;

&lt;p&gt;Unmounting is only half of it. The other half — the modal closing while the component stays mounted — is yours either way, which is exactly why the pattern above binds &lt;code&gt;setLocked&lt;/code&gt; to &lt;code&gt;open&lt;/code&gt; with a cleanup instead of toggling it from two handlers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setLocked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setLocked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLocked&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Think of the setter as owning a style you borrowed. Every borrow needs a return.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. One owner per element
&lt;/h3&gt;

&lt;p&gt;Two hook instances locking the same element is the subtlest failure mode, because each keeps its &lt;em&gt;own&lt;/em&gt; memory of the original &lt;code&gt;overflow&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A.lock()    → overflow: hidden    (A remembered "auto")
B.lock()    → overflow: hidden    (B remembered "hidden" 😬)
A.unlock()  → overflow: auto      (page scrolls, though B still thinks it's locked)
B.unlock()  → overflow: hidden    (page is now stuck, with nothing open)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing is going to save you here — this is inherent to "save the old value, put it back" and applies to every hand-rolled lock and most libraries. The answer is architectural: &lt;strong&gt;one lock owner per element.&lt;/strong&gt; Put the &lt;code&gt;useScrollLock(() =&amp;gt; document.body)&lt;/code&gt; call in your layout, provider, or store, and let modals ask it to lock rather than each carrying its own.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;code&gt;initialState: true&lt;/code&gt; skips the iOS guard
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;useScrollLock(target, true)&lt;/code&gt; applies &lt;code&gt;overflow: hidden&lt;/code&gt; from the first commit — but the &lt;code&gt;touchmove&lt;/code&gt; listener is only attached inside &lt;code&gt;lock()&lt;/code&gt;, which never ran. So a page that starts locked is still rubber-band-scrollable on iOS. Start &lt;code&gt;false&lt;/code&gt; and flip it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[,&lt;/span&gt; &lt;span class="nx"&gt;setLocked&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useScrollLock&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;setLocked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;setLocked&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// locked from mount, guard included&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Desktop layout shift
&lt;/h3&gt;

&lt;p&gt;Hiding the scrollbar reclaims ~15px and the entire page shifts sideways. That's not the hook's job to fix, and it's one CSS line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;html&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;scrollbar-gutter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;stable&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;h3&gt;
  
  
  5. Pass a getter, not &lt;code&gt;document.body&lt;/code&gt;, for SSR
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;useScrollLock(document.body)&lt;/code&gt; evaluates &lt;code&gt;document.body&lt;/code&gt; &lt;strong&gt;during render&lt;/strong&gt;, which throws on the server before the hook gets a chance to be careful. &lt;code&gt;() =&amp;gt; document.body&lt;/code&gt; (or a ref) is only read inside effects and handlers, where &lt;code&gt;getTargetElement&lt;/code&gt; already bails out without a &lt;code&gt;window&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[,&lt;/span&gt; &lt;span class="nx"&gt;setLocked&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useScrollLock&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ✅ SSR-safe&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[,&lt;/span&gt; &lt;span class="nx"&gt;setLocked&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useScrollLock&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;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// ❌ crashes on the server&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same rule applies to every hook in the library that takes an element target, and it's the single most common SSR mistake in Next.js and Remix apps.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. &lt;code&gt;hidden&lt;/code&gt; stops gestures, not programmatic scrolling
&lt;/h3&gt;

&lt;p&gt;An &lt;code&gt;overflow: hidden&lt;/code&gt; box is still scrollable via &lt;code&gt;scrollTop&lt;/code&gt;, &lt;code&gt;scrollTo&lt;/code&gt;, &lt;code&gt;scrollIntoView&lt;/code&gt; — and, crucially, by the browser scrolling a newly focused element into view. If focus escapes to a link behind your modal, your "locked" page will scroll to it. Scroll locking and focus trapping are two halves of the same feature; ship both.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Not to Use useScrollLock
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You only need to stop an inner scroller from chaining to the page&lt;/strong&gt; → &lt;code&gt;overscroll-behavior: contain&lt;/code&gt; in CSS, no JavaScript at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're using &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; and can require Chrome 144+&lt;/strong&gt; → &lt;code&gt;overscroll-behavior: contain&lt;/code&gt; on the dialog and its &lt;code&gt;::backdrop&lt;/code&gt; is less code than any hook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want to scroll &lt;em&gt;to&lt;/em&gt; something&lt;/strong&gt; → &lt;a href="https://reactuse.com/browser/usescrollintoview/" rel="noopener noreferrer"&gt;&lt;code&gt;useScrollIntoView&lt;/code&gt;&lt;/a&gt;, or the native one-liner — &lt;a href="https://reactuse.com/blog/react-scrollintoview-useref/" rel="noopener noreferrer"&gt;yesterday's post on scrollIntoView with useRef&lt;/a&gt; covers both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want to read or react to scroll position&lt;/strong&gt; → &lt;a href="https://reactuse.com/browser/usescroll/" rel="noopener noreferrer"&gt;&lt;code&gt;useScroll&lt;/code&gt;&lt;/a&gt; or &lt;a href="https://reactuse.com/element/usewindowscroll/" rel="noopener noreferrer"&gt;&lt;code&gt;useWindowScroll&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want a genuinely immersive, chrome-free view&lt;/strong&gt; → &lt;a href="https://reactuse.com/browser/usefullscreen/" rel="noopener noreferrer"&gt;&lt;code&gt;useFullscreen&lt;/code&gt;&lt;/a&gt; instead of locking a scroll container.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're loading more rows as the user scrolls&lt;/strong&gt; → &lt;a href="https://reactuse.com/browser/useinfinitescroll/" rel="noopener noreferrer"&gt;&lt;code&gt;useInfiniteScroll&lt;/code&gt;&lt;/a&gt;; the last thing you want there is a lock.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;overflow: hidden&lt;/code&gt; is the right mechanism on desktop and an incomplete one on iOS Safari, where only cancelling &lt;code&gt;touchmove&lt;/code&gt; (with &lt;code&gt;passive: false&lt;/code&gt;) actually stops the document from rubber-banding.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://reactuse.com/browser/usescrolllock/" rel="noopener noreferrer"&gt;&lt;code&gt;useScrollLock&lt;/code&gt;&lt;/a&gt; pairs that guard with a scroll-aware ancestor check, so the page can't move while your modal's own content still scrolls — and multi-touch zoom survives.&lt;/li&gt;
&lt;li&gt;It restores the exact inline &lt;code&gt;overflow&lt;/code&gt; it replaced, exposes the lock as state you can render off, and works on any element, which is what you need when your app scrolls inside a div rather than the document.&lt;/li&gt;
&lt;li&gt;Bind the lock to the state that describes your UI (&lt;code&gt;setLocked(open)&lt;/code&gt; plus a cleanup), keep &lt;strong&gt;one owner per element&lt;/strong&gt;, start &lt;code&gt;initialState&lt;/code&gt; at &lt;code&gt;false&lt;/code&gt;, pass a getter for SSR, and add &lt;code&gt;scrollbar-gutter: stable&lt;/code&gt; for the desktop shift.&lt;/li&gt;
&lt;li&gt;Scroll locking is half a modal. Trap focus too, or &lt;code&gt;hidden&lt;/code&gt; will still scroll when something behind the overlay takes focus.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;useScrollLock&lt;/code&gt;, &lt;code&gt;useDisclosure&lt;/code&gt;, &lt;code&gt;useScrollIntoView&lt;/code&gt;, and 110+ other SSR-safe, TypeScript-first hooks live in &lt;a href="https://reactuse.com" rel="noopener noreferrer"&gt;&lt;code&gt;@reactuses/core&lt;/code&gt;&lt;/a&gt; — one install, tree-shakeable, no dependencies to babysit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @reactuses/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>React scrollIntoView with useRef: Scroll to an Element (2026)</title>
      <dc:creator>ReactUse</dc:creator>
      <pubDate>Tue, 18 Aug 2026 01:54:50 +0000</pubDate>
      <link>https://dev.to/childrentime/react-scrollintoview-with-useref-scroll-to-an-element-2026-4ha4</link>
      <guid>https://dev.to/childrentime/react-scrollintoview-with-useref-scroll-to-an-element-2026-4ha4</guid>
      <description>&lt;p&gt;You have a long form. The user hits Submit, validation fails on a field three screens down, and the error message renders somewhere they can't see. The fix is one browser API call — but &lt;em&gt;where&lt;/em&gt; you put it, and what you pass it, is where an afternoon goes.&lt;/p&gt;

&lt;p&gt;The short answer, which is what most people are here for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Article&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;sectionRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLDivElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&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="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sectionRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;scrollIntoView&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;behavior&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;smooth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        Jump to details
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* … a lot of content … */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;sectionRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Details&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;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;That's the whole pattern: a ref on the element, &lt;code&gt;.scrollIntoView()&lt;/code&gt; in the handler, &lt;code&gt;?.&lt;/code&gt; because &lt;code&gt;sectionRef.current&lt;/code&gt; is &lt;code&gt;null&lt;/code&gt; until React commits. It's built into every browser, it costs nothing, and for a static anchor like this it's the right answer — don't reach for a library.&lt;/p&gt;

&lt;p&gt;This post covers the rest of it: what the arguments actually do, the sticky-header offset problem (and why the CSS answer beats the JavaScript one), how to scroll to something that was &lt;em&gt;just&lt;/em&gt; rendered, and the four things the native call genuinely can't do — at which point &lt;a href="https://reactuse.com/browser/usescrollintoview/" rel="noopener noreferrer"&gt;&lt;code&gt;useScrollIntoView&lt;/code&gt;&lt;/a&gt; from &lt;a href="https://reactuse.com" rel="noopener noreferrer"&gt;&lt;code&gt;@reactuses/core&lt;/code&gt;&lt;/a&gt; earns its place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Arguments You Actually Have
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Element.scrollIntoView()&lt;/code&gt; takes one optional options object with three keys:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Values&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;block&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;start&lt;/code&gt; · &lt;code&gt;center&lt;/code&gt; · &lt;code&gt;end&lt;/code&gt; · &lt;code&gt;nearest&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;start&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Alignment along the &lt;strong&gt;block&lt;/strong&gt; axis — vertical in a normal writing mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;inline&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;start&lt;/code&gt; · &lt;code&gt;center&lt;/code&gt; · &lt;code&gt;end&lt;/code&gt; · &lt;code&gt;nearest&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;nearest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Alignment along the &lt;strong&gt;inline&lt;/strong&gt; axis — horizontal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;behavior&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;auto&lt;/code&gt; · &lt;code&gt;instant&lt;/code&gt; · &lt;code&gt;smooth&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;auto&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;auto&lt;/code&gt; follows the CSS &lt;code&gt;scroll-behavior&lt;/code&gt; of the scrolling box&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So the three calls worth memorizing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scrollIntoView&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;                                        &lt;span class="c1"&gt;// snap it to the top&lt;/span&gt;
&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scrollIntoView&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;behavior&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;smooth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// glide it to the middle&lt;/span&gt;
&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scrollIntoView&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nearest&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;                    &lt;span class="c1"&gt;// move only if it's off-screen&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;block: "nearest"&lt;/code&gt; is the underrated one. It scrolls the &lt;em&gt;minimum&lt;/em&gt; distance needed to bring the element into view and does nothing at all if the element is already visible — exactly what you want for keyboard navigation in a listbox, where re-centering on every arrow key makes the list feel like it's fighting you.&lt;/p&gt;

&lt;p&gt;There's also a legacy boolean form: &lt;code&gt;scrollIntoView(true)&lt;/code&gt; means &lt;code&gt;block: "start"&lt;/code&gt;, &lt;code&gt;scrollIntoView(false)&lt;/code&gt; means &lt;code&gt;block: "end"&lt;/code&gt;. It still works everywhere; the object form says what it means.&lt;/p&gt;

&lt;p&gt;One thing that surprises people: &lt;code&gt;scrollIntoView&lt;/code&gt; scrolls &lt;strong&gt;every scrollable ancestor&lt;/strong&gt;, not just the nearest one. If your element sits in a scrollable panel inside a scrollable page, both move so the element ends up visible. That's almost always what you wanted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sticky Headers: Use CSS, Not a Magic Number
&lt;/h2&gt;

&lt;p&gt;The single most common follow-up: you scroll to a heading, and your 64px sticky header sits right on top of it.&lt;/p&gt;

&lt;p&gt;The instinct is to compute it by hand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// don't&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollY&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scrollTo&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;behavior&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;smooth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you own that &lt;code&gt;64&lt;/code&gt;. It's wrong on mobile where the header is shorter, wrong when a promo banner appears above it, wrong when the element is inside a scroll container rather than the page, and you've given up &lt;code&gt;scrollIntoView&lt;/code&gt;'s ancestor handling to boot.&lt;/p&gt;

&lt;p&gt;The platform has a property for exactly this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.section&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;scroll-margin-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* or var(--header-height) */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;scroll-margin-top&lt;/code&gt; tells the browser to treat the element as if it had that much extra margin &lt;em&gt;for scrolling purposes only&lt;/em&gt;. Plain &lt;code&gt;el.scrollIntoView({ behavior: "smooth" })&lt;/code&gt; then stops 5rem short, layout is untouched, and the value lives next to the header height it depends on. It also fixes &lt;code&gt;:target&lt;/code&gt; anchors and browser find-in-page for free, which the JavaScript version never will.&lt;/p&gt;

&lt;p&gt;Reach for &lt;code&gt;scroll-margin-top&lt;/code&gt; first. Every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scrolling to Something That Just Rendered
&lt;/h2&gt;

&lt;p&gt;The other half of the problem is timing. You add an item to a list and want to scroll to it; you open an accordion and want to reveal it; you set an error and want to jump to it. The naive version doesn't work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// broken: the DOM doesn't have the new row yet&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;addRow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setRows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&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;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;newRow&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="nx"&gt;lastRowRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;scrollIntoView&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// still the *old* last row, or null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;setRows&lt;/code&gt; schedules a render. React commits it later — and under React 18+ concurrent rendering, "later" is genuinely not this tick. At the moment that line runs, the DOM is still the old DOM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The default fix is an effect.&lt;/strong&gt; Scroll after the commit that added the row:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;lastRowRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;scrollIntoView&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;behavior&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;smooth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nearest&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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;useLayoutEffect&lt;/code&gt; instead if you want an &lt;em&gt;instant&lt;/em&gt; scroll to land before the browser paints — otherwise the user sees one frame at the old position, which reads as a flicker. For a smooth scroll it doesn't matter; the animation starts either way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Callback refs are cleaner for "the element I just created".&lt;/strong&gt; No effect, no dependency array, no ref to keep in sync — the callback fires the moment React attaches the node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scrollOnMount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLElement&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;scrollIntoView&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;behavior&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;smooth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nearest&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="p"&gt;[]);&lt;/span&gt;

&lt;span class="c1"&gt;// …&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Row&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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="nx"&gt;scrollOnMount&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;))}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;flushSync&lt;/code&gt; is the escape hatch, not the default.&lt;/strong&gt; If you truly must scroll in the same event handler that changed the state, you can force the commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;flushSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react-dom&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;flushSync&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setExpanded&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="nx"&gt;detailsRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;scrollIntoView&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;behavior&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;smooth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works, and it costs you the batching and concurrency React was doing on your behalf. Fine as a one-off in a handler; a smell if it shows up three times in a file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the Native Call Runs Out
&lt;/h2&gt;

&lt;p&gt;For anchors, "scroll to the error", and keyboard list navigation, everything above is enough and you should stop reading. Four things it genuinely cannot do:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. You can't control the duration or the curve.&lt;/strong&gt; &lt;code&gt;behavior: "smooth"&lt;/code&gt; is whatever the browser decides — different speed in Chrome and Firefox, and no knob at all. If the scroll is part of a choreographed transition that has to line up with a 400ms fade, you can't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. There's no reliable "it finished" callback.&lt;/strong&gt; The &lt;code&gt;scrollend&lt;/code&gt; event was designed for this and landed in Chrome/Edge 114 and Firefox 109, with Safari following later — check support before you depend on it, and note it doesn't tell you &lt;em&gt;which&lt;/em&gt; programmatic scroll ended. The workarounds people ship instead (a &lt;code&gt;setTimeout&lt;/code&gt; guess, polling &lt;code&gt;scrollY&lt;/code&gt; until it stops changing) are exactly as fragile as they sound.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. You can't cancel it.&lt;/strong&gt; Start a long smooth scroll, and if the user grabs the wheel halfway down, the browser keeps dragging them to the destination. On a long page this is the single most annoying scroll bug there is, and there is no API to stop it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. It ignores &lt;code&gt;prefers-reduced-motion&lt;/code&gt;.&lt;/strong&gt; Browsers do not universally downgrade &lt;code&gt;behavior: "smooth"&lt;/code&gt; for users who asked for reduced motion — that's on you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reduce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;matchMedia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;(prefers-reduced-motion: reduce)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;matches&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scrollIntoView&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;behavior&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reduce&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;smooth&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Easy to write once, easy to forget in the other eleven places you scroll.&lt;/p&gt;

&lt;h2&gt;
  
  
  useScrollIntoView
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://reactuse.com/browser/usescrollintoview/" rel="noopener noreferrer"&gt;&lt;code&gt;useScrollIntoView&lt;/code&gt;&lt;/a&gt; runs the animation itself on &lt;code&gt;requestAnimationFrame&lt;/code&gt;, which is what buys back all four:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @reactuses/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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;useRef&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useScrollIntoView&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@reactuses/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Article&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;targetRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLParagraphElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;scrollIntoView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cancel&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useScrollIntoView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;onScrollFinish&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;targetRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;scrollIntoView&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Jump to details&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;150vh&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;targetRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;tabIndex&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Details&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;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;&lt;code&gt;useScrollIntoView(target, options?, scrollContainer?)&lt;/code&gt; returns &lt;code&gt;{ scrollIntoView, cancel }&lt;/code&gt;. It's SSR-safe — nothing touches the DOM until you call it — and the target can be a ref, an element, or a getter function, so it works with whatever you already have.&lt;/p&gt;

&lt;p&gt;The options, all optional:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;duration&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1250&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Milliseconds. &lt;code&gt;0&lt;/code&gt; jumps instantly.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;easing&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;easeInOutQuad&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Any &lt;code&gt;(t: number) =&amp;gt; number&lt;/code&gt; over &lt;code&gt;0…1&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;axis&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;"y"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"x"&lt;/code&gt; for horizontal scrollers. One axis per hook.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;offset&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extra distance from the edge — the sticky-header allowance.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cancelable&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Wheel or touch input aborts the animation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;isList&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Skip the scroll when the target is already in view.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;onScrollFinish&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Fires when the animation settles.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And the alignment goes on the call, not the config, because it's usually per-invocation: &lt;code&gt;scrollIntoView({ alignment: "start" | "center" | "end" })&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cancelable is the one you'll actually feel
&lt;/h3&gt;

&lt;p&gt;With &lt;code&gt;cancelable: true&lt;/code&gt; (the default) the hook watches for &lt;code&gt;wheel&lt;/code&gt; and &lt;code&gt;touchmove&lt;/code&gt; and stops the animation where it is. The user reaches for the scrollbar mid-flight and the page just… lets them. Compare that with &lt;code&gt;behavior: "smooth"&lt;/code&gt;, which will happily fight a user for a full second.&lt;/p&gt;

&lt;p&gt;You can also stop it yourself — closing the modal that triggered the scroll, say:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;scrollIntoView&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cancel&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useScrollIntoView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetRef&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// it also cancels on unmount&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Reduced motion is handled
&lt;/h3&gt;

&lt;p&gt;The hook reads &lt;code&gt;prefers-reduced-motion&lt;/code&gt; internally via &lt;a href="https://reactuse.com/browser/usereducedmotion/" rel="noopener noreferrer"&gt;&lt;code&gt;useReducedMotion&lt;/code&gt;&lt;/a&gt;. When the user has asked for less motion, the easing collapses to its final value and the scroll becomes an instant jump — same destination, same &lt;code&gt;onScrollFinish&lt;/code&gt;, no animation. You don't write the branch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scrolling inside a container, and sideways
&lt;/h3&gt;

&lt;p&gt;Pass a scroll container as the third argument when you want to move a specific element's scroll position rather than the page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLDivElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&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;itemRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLLIElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;scrollIntoView&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useScrollIntoView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;itemRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;isList&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="nx"&gt;listRef&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without the third argument the hook walks up from the target and picks the first ancestor whose computed &lt;code&gt;overflow-x&lt;/code&gt;/&lt;code&gt;overflow-y&lt;/code&gt; is &lt;code&gt;auto&lt;/code&gt; or &lt;code&gt;scroll&lt;/code&gt;, falling back to the page. That auto-detection is convenient and correct most of the time; pass the container explicitly when you know it.&lt;/p&gt;

&lt;p&gt;For a carousel, flip the axis:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;scrollIntoView&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useScrollIntoView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slideRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;axis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;trackRef&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;scrollIntoView&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Scroll to the first invalid field
&lt;/h3&gt;

&lt;p&gt;The pattern that started this post, with the pieces in the right places:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;CheckoutForm&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setErrors&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;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="nx"&gt;firstErrorRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLDivElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;scrollIntoView&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useScrollIntoView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;firstErrorRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// clear the sticky header&lt;/span&gt;
    &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;onScrollFinish&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;firstErrorRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;input&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nf"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FormEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setErrors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;scrollIntoView&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;alignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;start&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;firstErrorField&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;errors&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="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="na"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onSubmit&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;FIELDS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Field&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;firstErrorField&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;firstErrorRef&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the hook resolves the target when you &lt;em&gt;call&lt;/em&gt; &lt;code&gt;scrollIntoView&lt;/code&gt; — not when it renders — calling it in the same handler as &lt;code&gt;setErrors&lt;/code&gt; works even though &lt;code&gt;firstErrorRef&lt;/code&gt; is attached by the render that &lt;code&gt;setErrors&lt;/code&gt; triggers. No &lt;code&gt;flushSync&lt;/code&gt;, no effect. Moving focus in &lt;code&gt;onScrollFinish&lt;/code&gt; rather than immediately means screen-reader users and sighted users arrive at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas Worth Knowing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;offset&lt;/code&gt; doesn't apply to &lt;code&gt;alignment: "center"&lt;/code&gt;.&lt;/strong&gt; It's an allowance measured from the &lt;em&gt;nearest edge&lt;/em&gt;, so it only affects &lt;code&gt;"start"&lt;/code&gt; and &lt;code&gt;"end"&lt;/code&gt;. Centering something under a sticky header means either using &lt;code&gt;"start"&lt;/code&gt; with an offset, or accepting the center. This one silently does nothing if you assume otherwise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't combine it with &lt;code&gt;scroll-behavior: smooth&lt;/code&gt;.&lt;/strong&gt; The hook animates by assigning &lt;code&gt;scrollTop&lt;/code&gt;/&lt;code&gt;scrollLeft&lt;/code&gt; every frame. If CSS also says that box scrolls smoothly, the browser tries to animate each of those ~60 assignments and the result is a stuttering mess. Pick one: CSS smooth scrolling &lt;em&gt;or&lt;/em&gt; this hook, per container.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One axis per hook.&lt;/strong&gt; &lt;code&gt;axis&lt;/code&gt; is &lt;code&gt;"x"&lt;/code&gt; or &lt;code&gt;"y"&lt;/code&gt;, not both. A grid that needs diagonal movement needs two hooks, or the native call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The auto-detected scroll parent is cached per element.&lt;/strong&gt; The first lookup for a given node is remembered. If your layout toggles &lt;code&gt;overflow&lt;/code&gt; on an ancestor at runtime — a panel that becomes scrollable only when expanded — pass the container as the third argument instead of relying on detection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cancelable&lt;/code&gt; covers wheel and touch, not keys.&lt;/strong&gt; Page Down and the scrollbar don't abort the animation. It's the common case, not every case; call &lt;code&gt;cancel()&lt;/code&gt; yourself from a keydown handler if that matters to you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;isList&lt;/code&gt; is directional.&lt;/strong&gt; With &lt;code&gt;isList: true&lt;/code&gt; the hook only moves when the target is outside the container on the side implied by &lt;code&gt;alignment&lt;/code&gt; — a target already visible produces no scroll at all. That's the point (it stops a keyboard-navigated list from jittering on every keystroke), but it means &lt;code&gt;isList: true&lt;/code&gt; with the wrong alignment can look like the hook is ignoring you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;duration: 0&lt;/code&gt; is an instant jump, not a no-op.&lt;/strong&gt; Useful for honouring your own "no animations" setting without branching on which function to call.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When to Skip the Hook
&lt;/h2&gt;

&lt;p&gt;Native &lt;code&gt;scrollIntoView&lt;/code&gt; is the right call more often than not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A static anchor or a table-of-contents link&lt;/strong&gt; → &lt;code&gt;el.scrollIntoView({ behavior: "smooth" })&lt;/code&gt; plus &lt;code&gt;scroll-margin-top&lt;/code&gt;. No dependency, no animation loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyboard navigation in a listbox&lt;/strong&gt; → &lt;code&gt;block: "nearest"&lt;/code&gt; does the minimum-movement behaviour natively, and instant is the correct feel there anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need to align on both axes at once&lt;/strong&gt; → the native call takes &lt;code&gt;block&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; &lt;code&gt;inline&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're scrolling to a position, not an element&lt;/strong&gt; → &lt;code&gt;window.scrollTo&lt;/code&gt; / &lt;code&gt;el.scrollTo&lt;/code&gt;, or &lt;a href="https://reactuse.com/browser/usescroll/" rel="noopener noreferrer"&gt;&lt;code&gt;useScroll&lt;/code&gt;&lt;/a&gt; to read and react to scroll position.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want to know what's on screen rather than move to it&lt;/strong&gt; → &lt;a href="https://reactuse.com/element/useintersectionobserver/" rel="noopener noreferrer"&gt;&lt;code&gt;useIntersectionObserver&lt;/code&gt;&lt;/a&gt;, which is also how you highlight the current section in a TOC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want to stop the page scrolling entirely&lt;/strong&gt; (modal open) → &lt;a href="https://reactuse.com/browser/usescrolllock/" rel="noopener noreferrer"&gt;&lt;code&gt;useScrollLock&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The baseline is three lines: &lt;code&gt;useRef&lt;/code&gt; on the element, &lt;code&gt;ref.current?.scrollIntoView({ behavior: "smooth" })&lt;/code&gt; in the handler, &lt;code&gt;?.&lt;/code&gt; because the ref is &lt;code&gt;null&lt;/code&gt; before commit. Learn &lt;code&gt;block: "nearest"&lt;/code&gt; — it's the one you'll use most.&lt;/li&gt;
&lt;li&gt;Solve sticky-header overlap with &lt;code&gt;scroll-margin-top&lt;/code&gt; in CSS, not by subtracting a hard-coded pixel value from &lt;code&gt;getBoundingClientRect()&lt;/code&gt;. It survives responsive headers and fixes &lt;code&gt;:target&lt;/code&gt; anchors too.&lt;/li&gt;
&lt;li&gt;To scroll to something you just rendered, scroll in an effect keyed on the change, or use a callback ref. &lt;code&gt;flushSync&lt;/code&gt; works but gives up batching — keep it as an escape hatch.&lt;/li&gt;
&lt;li&gt;Native smooth scrolling has no duration control, no dependable completion event, no cancel, and no &lt;code&gt;prefers-reduced-motion&lt;/code&gt; handling. If none of those matter, don't add a dependency.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://reactuse.com/browser/usescrollintoview/" rel="noopener noreferrer"&gt;&lt;code&gt;useScrollIntoView&lt;/code&gt;&lt;/a&gt; covers exactly those gaps — configurable &lt;code&gt;duration&lt;/code&gt;/&lt;code&gt;easing&lt;/code&gt;, &lt;code&gt;onScrollFinish&lt;/code&gt;, wheel-and-touch cancellation, automatic reduced-motion fallback, plus &lt;code&gt;offset&lt;/code&gt;, horizontal &lt;code&gt;axis&lt;/code&gt;, an explicit scroll container, and &lt;code&gt;isList&lt;/code&gt; for jitter-free list navigation. It resolves the target at call time, so it works in the same handler as the &lt;code&gt;setState&lt;/code&gt; that rendered it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;useScrollIntoView&lt;/code&gt;, &lt;code&gt;useScroll&lt;/code&gt;, &lt;code&gt;useScrollLock&lt;/code&gt;, and 110+ other SSR-safe, TypeScript-first hooks live in &lt;a href="https://reactuse.com" rel="noopener noreferrer"&gt;&lt;code&gt;@reactuses/core&lt;/code&gt;&lt;/a&gt; — one install, tree-shakeable, no dependencies to babysit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @reactuses/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>React useSessionStorage Hook: Per-Tab State That Survives Reloads (2026)</title>
      <dc:creator>ReactUse</dc:creator>
      <pubDate>Mon, 17 Aug 2026 02:35:15 +0000</pubDate>
      <link>https://dev.to/childrentime/react-usesessionstorage-hook-per-tab-state-that-survives-reloads-2026-1nd3</link>
      <guid>https://dev.to/childrentime/react-usesessionstorage-hook-per-tab-state-that-survives-reloads-2026-1nd3</guid>
      <description>&lt;p&gt;Here's a checkout flow that loses the customer at step three:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Checkout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setStep&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setForm&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CheckoutForm&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;EMPTY_FORM&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// step 1: address, step 2: shipping, step 3: payment…&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The customer fills in their address, picks a shipping option, and on the payment step the provider redirects them out to a 3-D Secure page and back. Or they just hit refresh. Either way, &lt;code&gt;step&lt;/code&gt; is &lt;code&gt;0&lt;/code&gt; again and &lt;code&gt;form&lt;/code&gt; is empty. &lt;code&gt;useState&lt;/code&gt; lives exactly as long as the component instance does — a reload, a redirect, a full-page navigation, and it's gone.&lt;/p&gt;

&lt;p&gt;Everyone knows the fix is Web Storage. Most people reach for &lt;code&gt;localStorage&lt;/code&gt;, and it works — until it works too well. The half-finished checkout is now sitting in every tab the customer opens, it's still there next week when they come back for something else, and if they open two tabs to compare shipping options, &lt;a href="https://reactuse.com/state/uselocalstorage/" rel="noopener noreferrer"&gt;&lt;code&gt;useLocalStorage&lt;/code&gt;&lt;/a&gt; faithfully syncs the two forms into each other. What you actually wanted was state that survives &lt;em&gt;this tab's&lt;/em&gt; reloads and redirects and then disappears when the tab does. That's &lt;code&gt;sessionStorage&lt;/code&gt;, and &lt;a href="https://reactuse.com/state/usesessionstorage/" rel="noopener noreferrer"&gt;&lt;code&gt;useSessionStorage&lt;/code&gt;&lt;/a&gt; from &lt;a href="https://reactuse.com" rel="noopener noreferrer"&gt;&lt;code&gt;@reactuses/core&lt;/code&gt;&lt;/a&gt; is the &lt;code&gt;useState&lt;/code&gt;-shaped hook for it. This post covers what &lt;code&gt;sessionStorage&lt;/code&gt; really promises (and doesn't), when to choose it over &lt;code&gt;localStorage&lt;/code&gt; and cookies, the four patterns it's built for, and the gotchas — hydration, tab restore, &lt;code&gt;window.open&lt;/code&gt; — that bite the hand-rolled version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @reactuses/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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;useSessionStorage&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@reactuses/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Checkout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setStep&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSessionStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkout:step&lt;/span&gt;&lt;span class="dl"&gt;"&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setForm&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useSessionStorage&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CheckoutForm&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkout:form&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;EMPTY_FORM&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Wizard&lt;/span&gt; &lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onNext&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&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="nf"&gt;setStep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&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;s&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&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="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AddressStep&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;address&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setForm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;address&lt;/span&gt; &lt;span class="p"&gt;}))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* … */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Wizard&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;useSessionStorage(key, defaultValue)&lt;/code&gt; returns the same &lt;code&gt;[value, setValue]&lt;/code&gt; tuple as &lt;code&gt;useState&lt;/code&gt;, with the same functional updates. The value is read from &lt;code&gt;sessionStorage&lt;/code&gt; on mount, written back on every update, and typed &lt;code&gt;T | null&lt;/code&gt; — &lt;code&gt;null&lt;/code&gt; because &lt;code&gt;setValue(null)&lt;/code&gt; removes the key (more on that below). Reload the page, get redirected to a payment provider and back, navigate away and hit the browser's back button: &lt;code&gt;step&lt;/code&gt; and &lt;code&gt;form&lt;/code&gt; are exactly where the customer left them. Close the tab: they're gone, which is the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  What sessionStorage Actually Promises
&lt;/h2&gt;

&lt;p&gt;The name misleads people into thinking "session" means "logged-in session" or "browser session". It means &lt;strong&gt;one top-level browsing context — a tab or window — for one origin&lt;/strong&gt;. Concretely:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Survives?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reload / hard refresh&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client-side route change (SPA)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full-page navigation to another page on the same origin&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redirect to a third-party site and back (OAuth, payment, SSO)&lt;/td&gt;
&lt;td&gt;✅ — same tab, same origin on return&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser back / forward&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open the same URL in a &lt;strong&gt;new tab&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;❌ fresh, empty storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Close the tab&lt;/td&gt;
&lt;td&gt;❌ cleared (with a caveat: browsers that restore closed tabs restore its &lt;code&gt;sessionStorage&lt;/code&gt; too)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Close the browser&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two edge cases surprise people. First, &lt;strong&gt;&lt;code&gt;window.open()&lt;/code&gt; copies&lt;/strong&gt; the opener's &lt;code&gt;sessionStorage&lt;/code&gt; into the new window (per the HTML spec, whenever the new window keeps an &lt;code&gt;opener&lt;/code&gt;), and Chrome's "Duplicate tab" copies it too — but it's a one-time snapshot, not a live link; the two tabs diverge from then on. Modern browsers open &lt;code&gt;target="_blank"&lt;/code&gt; links with &lt;code&gt;noopener&lt;/code&gt; by default, so ordinary links start clean. Second, &lt;code&gt;sessionStorage&lt;/code&gt; is &lt;strong&gt;shared with same-origin iframes in the same tab&lt;/strong&gt; — they're the same browsing context group — which is the only place the browser's native &lt;code&gt;storage&lt;/code&gt; event has any meaning for it (below).&lt;/p&gt;

&lt;p&gt;The rest is the same contract as &lt;code&gt;localStorage&lt;/code&gt;: synchronous, string-only, roughly 5 MB per origin, and readable by any script on the page — so it's &lt;strong&gt;not a security boundary&lt;/strong&gt;. It's &lt;em&gt;shorter-lived&lt;/em&gt; than &lt;code&gt;localStorage&lt;/code&gt;, which limits the blast radius of a leak, but XSS reads it just as easily. Anything that must be secret from JavaScript belongs in an &lt;code&gt;httpOnly&lt;/code&gt; cookie, not here.&lt;/p&gt;

&lt;h2&gt;
  
  
  useSessionStorage vs useLocalStorage vs useCookie vs useState
&lt;/h2&gt;

&lt;p&gt;Pick by &lt;em&gt;where&lt;/em&gt; the value should live and &lt;em&gt;how long&lt;/em&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You need state that…&lt;/th&gt;
&lt;th&gt;Reach for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;lives as long as the component&lt;/td&gt;
&lt;td&gt;&lt;code&gt;useState&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;survives reloads and redirects &lt;strong&gt;in this tab&lt;/strong&gt;, then disappears&lt;/td&gt;
&lt;td&gt;&lt;a href="https://reactuse.com/state/usesessionstorage/" rel="noopener noreferrer"&gt;&lt;code&gt;useSessionStorage&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;survives browser restarts and stays in sync &lt;strong&gt;across tabs&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;a href="https://reactuse.com/state/uselocalstorage/" rel="noopener noreferrer"&gt;&lt;code&gt;useLocalStorage&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;the &lt;strong&gt;server&lt;/strong&gt; needs on the first request&lt;/td&gt;
&lt;td&gt;&lt;a href="https://reactuse.com/state/usecookie/" rel="noopener noreferrer"&gt;&lt;code&gt;useCookie&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;is messaged between tabs, not stored&lt;/td&gt;
&lt;td&gt;&lt;a href="https://reactuse.com/browser/usebroadcastchannel/" rel="noopener noreferrer"&gt;&lt;code&gt;useBroadcastChannel&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rule of thumb that resolves 90% of "local or session?" debates: &lt;strong&gt;if two tabs showing different values would be a bug, use &lt;code&gt;localStorage&lt;/code&gt;; if two tabs showing the same value would be a bug, use &lt;code&gt;sessionStorage&lt;/code&gt;.&lt;/strong&gt; Theme, language, "don't show this again forever" — a user expects those to be one value everywhere, so local. A half-completed form, the filters on &lt;em&gt;this&lt;/em&gt; dashboard view, the page you were on before an auth redirect — those belong to one tab, so session.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;useSessionStorage&lt;/code&gt; and &lt;code&gt;useLocalStorage&lt;/code&gt; share &lt;strong&gt;the exact same API, serialization, and internals&lt;/strong&gt; — swap the import and the lifetime changes, nothing else does. Everything in the &lt;a href="https://reactuse.com/blog/react-uselocalstorage-hook/" rel="noopener noreferrer"&gt;useLocalStorage deep-dive&lt;/a&gt; about hydration, &lt;code&gt;setValue(null)&lt;/code&gt;, custom serializers and &lt;code&gt;onError&lt;/code&gt; applies verbatim, so I'll only recap the parts that matter and spend the rest on the session-specific patterns and gotchas.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Get Over the Hand-Rolled Version
&lt;/h2&gt;

&lt;p&gt;Every codebase has a &lt;code&gt;useState&lt;/code&gt; initializer that reads storage plus a &lt;code&gt;useEffect&lt;/code&gt; that writes it back. Here's what that version gets wrong and &lt;code&gt;useSessionStorage&lt;/code&gt; gets right:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SSR and hydration.&lt;/strong&gt; The hook is built on &lt;code&gt;useSyncExternalStore&lt;/code&gt; with a server snapshot that returns the default. It never touches &lt;code&gt;window&lt;/code&gt; on the server, and the client's first render matches the server HTML, then re-renders with the stored value through the proper path — no crash, no hydration-mismatch warning, no &lt;code&gt;typeof window&lt;/code&gt; guard in your code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serialization by default type.&lt;/strong&gt; Pass a number and you get a number back; pass an object and it's &lt;code&gt;JSON.stringify&lt;/code&gt;/&lt;code&gt;JSON.parse&lt;/code&gt;; pass a &lt;code&gt;Map&lt;/code&gt;, &lt;code&gt;Set&lt;/code&gt; or &lt;code&gt;Date&lt;/code&gt; and they round-trip correctly (a plain &lt;code&gt;JSON.stringify(new Map())&lt;/code&gt; gives you &lt;code&gt;{}&lt;/code&gt;). Need a specific wire format? Provide &lt;code&gt;serializer: { read, write }&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;setValue(null)&lt;/code&gt; removes the key.&lt;/strong&gt; "Cleared" is a real state, distinct from "reset to default": after &lt;code&gt;setForm(null)&lt;/code&gt; the value is &lt;code&gt;null&lt;/code&gt;, and on the next mount it comes back as &lt;code&gt;EMPTY_FORM&lt;/code&gt;. That's your "start over" button, and it's why the type is &lt;code&gt;T | null&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Corrupted data doesn't crash.&lt;/strong&gt; Someone edits DevTools, an old deploy wrote a different shape, a &lt;code&gt;JSON.parse&lt;/code&gt; throws — the hook returns the default and reports through &lt;code&gt;onError&lt;/code&gt; (default &lt;code&gt;console.error&lt;/code&gt;) instead of taking the component down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage unavailable? Degrades to memory.&lt;/strong&gt; Some privacy modes and embedded contexts throw on storage access. The hook catches it, calls &lt;code&gt;onError&lt;/code&gt;, and behaves like plain &lt;code&gt;useState&lt;/code&gt; for the rest of the session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every component on the same key agrees.&lt;/strong&gt; Two &lt;code&gt;useSessionStorage("checkout:step", 0)&lt;/code&gt; calls — a progress bar in the header, the wizard body — re-render together on every write. The native &lt;code&gt;storage&lt;/code&gt; event never fires in the document that made the change, so the hand-rolled version drifts; the hook re-broadcasts each write internally so it can't.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Multi-step forms and wizards
&lt;/h3&gt;

&lt;p&gt;The intro's checkout, done properly. Two details worth copying: &lt;strong&gt;namespace your keys&lt;/strong&gt; (&lt;code&gt;checkout:step&lt;/code&gt;, &lt;code&gt;checkout:form&lt;/code&gt;) so a "start over" can clear them together and unrelated features on the same origin never collide, and store the &lt;em&gt;draft&lt;/em&gt; separately from what's been &lt;em&gt;submitted&lt;/em&gt;, so a successful order can wipe the draft without touching anything else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setStep&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSessionStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkout:step&lt;/span&gt;&lt;span class="dl"&gt;"&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setDraft&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useSessionStorage&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CheckoutForm&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkout:form&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;EMPTY_FORM&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;placeOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;setDraft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// remove the key — nothing lingers in the tab&lt;/span&gt;
  &lt;span class="nf"&gt;setStep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/thank-you&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a large form with a keystroke-per-field update rate, storage writes are synchronous but cheap (a few KB of JSON); if you'd rather batch them, wrap the field updates in &lt;a href="https://reactuse.com/effect/usedebouncefn/" rel="noopener noreferrer"&gt;&lt;code&gt;useDebounceFn&lt;/code&gt;&lt;/a&gt; and write the draft on the trailing edge.&lt;/p&gt;

&lt;h3&gt;
  
  
  Surviving a redirect round-trip
&lt;/h3&gt;

&lt;p&gt;OAuth, SSO, payment providers, "verify your email" links that come back to the app — anything that navigates the tab away and returns needs to stash "where was I?" somewhere that survives a full-page unload but shouldn't be shared with the tab next door. That's &lt;code&gt;sessionStorage&lt;/code&gt;'s home turf: it's where auth libraries like MSAL keep their PKCE verifier and &lt;code&gt;state&lt;/code&gt; by default, for exactly this reason.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useReturnTo&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;returnTo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setReturnTo&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useSessionStorage&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auth:returnTo&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&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;navigate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useNavigate&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;stashAndRedirect&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;setReturnTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;buildAuthorizeUrl&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;restore&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;returnTo&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;setReturnTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// consume it — one round-trip, one restore&lt;/span&gt;
    &lt;span class="nf"&gt;navigate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;stashAndRedirect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;restore&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;Two tabs, two logins, two different &lt;code&gt;returnTo&lt;/code&gt;s — no cross-talk. Had this been &lt;code&gt;localStorage&lt;/code&gt;, tab B's redirect would overwrite tab A's return path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Per-tab view state that must &lt;em&gt;not&lt;/em&gt; sync
&lt;/h3&gt;

&lt;p&gt;The case that catches &lt;code&gt;useLocalStorage&lt;/code&gt; fans off guard: a user opens two tabs of the same dashboard to compare "last 7 days" against "last 30 days". With &lt;code&gt;localStorage&lt;/code&gt; and cross-tab sync, changing the range in one tab changes it in the other, and the user is left thinking the app is haunted. Any view state that's about &lt;em&gt;this window&lt;/em&gt; — filters, sort column, expanded rows, which side panel is open — is a &lt;code&gt;sessionStorage&lt;/code&gt; value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;range&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setRange&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useSessionStorage&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;7d&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;30d&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;90d&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dashboard:range&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;7d&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload preserves it, a second tab starts from the default, and the two never fight. If you &lt;em&gt;also&lt;/em&gt; want a persisted "last used" default across sessions, keep that in &lt;code&gt;localStorage&lt;/code&gt; and read it as the session default — two hooks, two lifetimes, both explicit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Once per session
&lt;/h3&gt;

&lt;p&gt;Announcement banners, "we use cookies" notices, an onboarding tooltip — things a user should be able to dismiss for the duration of their visit without you promising to hide them forever:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ReleaseBanner&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;dismissed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setDismissed&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSessionStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;banner:v6.5-dismissed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dismissed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;aside&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      New in v6.5 — &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt; &lt;span class="na"&gt;href&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/changelog"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;see what changed&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;a&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setDismissed&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="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Dismiss&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;aside&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ship it in the key (&lt;code&gt;banner:v6.5-dismissed&lt;/code&gt;) so a new release gets a fresh banner without touching the old flag. The same shape works for "the user already saw the intro animation this session" — pair it with &lt;a href="https://reactuse.com/browser/usereducedmotion/" rel="noopener noreferrer"&gt;&lt;code&gt;useReducedMotion&lt;/code&gt;&lt;/a&gt; if the animation is the kind you should skip anyway.&lt;/p&gt;

&lt;h3&gt;
  
  
  A stable per-tab ID
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;sessionStorage&lt;/code&gt; is the only browser primitive that naturally gives you "one value per tab that survives reloads". That's precisely what you want for a tab identifier — tagging analytics events, correlating logs, or telling &lt;a href="https://reactuse.com/browser/usebroadcastchannel/" rel="noopener noreferrer"&gt;&lt;code&gt;useBroadcastChannel&lt;/code&gt;&lt;/a&gt; messages apart by sender. &lt;code&gt;mountStorageValue&lt;/code&gt; seeds the key on first mount only if it's absent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tabId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useSessionStorage&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tab:id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&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="na"&gt;mountStorageValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="c1"&gt;// null on the very first render, then a UUID that's stable across reloads of this tab&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Gotchas Worth Knowing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The default flashes before the stored value, once.&lt;/strong&gt; Under SSR the server can't see the browser's storage, so the first paint shows the default and the stored value arrives on the post-hydration render. For a wizard step that's a non-issue; for something like "which panel is open" you may want a skeleton until the value is in. The trade-offs are the same as for &lt;code&gt;localStorage&lt;/code&gt; — see &lt;a href="https://reactuse.com/blog/ssr-safe-react-hooks/" rel="noopener noreferrer"&gt;SSR-Safe React Hooks&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Cleared when the tab closes" has an asterisk.&lt;/strong&gt; Chrome, Firefox and Safari all restore &lt;code&gt;sessionStorage&lt;/code&gt; when the user reopens a closed tab or the browser restores a session after a crash. Don't rely on tab close as a &lt;em&gt;guaranteed&lt;/em&gt; wipe for anything sensitive; if it must go, &lt;code&gt;setValue(null)&lt;/code&gt; it yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New tab ≠ same tab.&lt;/strong&gt; Users who Ctrl-click your link into a new tab arrive with empty &lt;code&gt;sessionStorage&lt;/code&gt;. That's usually correct (they want a fresh view), but it means "the user has already dismissed the banner" and "the wizard is on step 3" don't carry over. If they should, that's a &lt;code&gt;localStorage&lt;/code&gt; value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;window.open()&lt;/code&gt; copies, then forks.&lt;/strong&gt; If you &lt;code&gt;window.open()&lt;/code&gt; a same-origin popup (a preview, a print view), it starts with a &lt;em&gt;copy&lt;/em&gt; of the opener's &lt;code&gt;sessionStorage&lt;/code&gt;. Writes in the popup don't reach the opener; use &lt;a href="https://reactuse.com/browser/usebroadcastchannel/" rel="noopener noreferrer"&gt;&lt;code&gt;useBroadcastChannel&lt;/code&gt;&lt;/a&gt; or &lt;code&gt;postMessage&lt;/code&gt; if they need to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;listenToStorageChanges&lt;/code&gt; is mostly moot for sessionStorage.&lt;/strong&gt; The native &lt;code&gt;storage&lt;/code&gt; event only reaches &lt;em&gt;other documents sharing the same area&lt;/em&gt; — for &lt;code&gt;sessionStorage&lt;/code&gt;, that's same-origin iframes in the same tab, not other tabs. Same-tab sync between components is a separate, always-on mechanism and isn't affected by the option; leave it at the default and forget about it unless you have iframes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not a vault.&lt;/strong&gt; It's JavaScript-readable storage. It's fine for a PKCE verifier (single-use, short-lived, and worthless without the authorization code) and for drafts and view state; it's the wrong place for a long-lived access token you'd be upset to see exfiltrated. Server-side sessions and &lt;code&gt;httpOnly&lt;/code&gt; cookies exist for that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage can be full or blocked.&lt;/strong&gt; Quota is small and shared with everything else on the origin; some embedded/private contexts throw on access. Both are reported through &lt;code&gt;onError&lt;/code&gt; and the hook keeps working in memory. Log it — a "my form reset" bug report often traces back to a &lt;code&gt;QuotaExceededError&lt;/code&gt; nobody looked at.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The value is &lt;code&gt;T | null&lt;/code&gt;, on purpose.&lt;/strong&gt; After &lt;code&gt;setValue(null)&lt;/code&gt; the key is gone and you get &lt;code&gt;null&lt;/code&gt;, not the default. If your code can't handle &lt;code&gt;null&lt;/code&gt;, either never call &lt;code&gt;setValue(null)&lt;/code&gt; (write the default instead) or normalize at the read site: &lt;code&gt;const s = step ?? 0&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When Not to Use useSessionStorage
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The value should be one thing everywhere, forever&lt;/strong&gt; (theme, locale, "never show again") → &lt;a href="https://reactuse.com/state/uselocalstorage/" rel="noopener noreferrer"&gt;&lt;code&gt;useLocalStorage&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The server needs it on the first request&lt;/strong&gt; (theme without a flash, A/B bucket, auth session) → &lt;a href="https://reactuse.com/state/usecookie/" rel="noopener noreferrer"&gt;&lt;code&gt;useCookie&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tabs need to &lt;em&gt;talk&lt;/em&gt;, not *store&lt;/strong&gt;* ("you were logged out in another tab") → &lt;a href="https://reactuse.com/browser/usebroadcastchannel/" rel="noopener noreferrer"&gt;&lt;code&gt;useBroadcastChannel&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're keeping a value across renders, not across reloads&lt;/strong&gt; → &lt;code&gt;useState&lt;/code&gt;, &lt;code&gt;useRef&lt;/code&gt;, or &lt;a href="https://reactuse.com/state/uselatest/" rel="noopener noreferrer"&gt;&lt;code&gt;useLatest&lt;/code&gt;&lt;/a&gt; — the &lt;a href="https://reactuse.com/blog/react-uselatest-hook/" rel="noopener noreferrer"&gt;previous post in this series&lt;/a&gt; covers when each applies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want it in the URL&lt;/strong&gt; (shareable filters, deep-linkable steps) → put it in the query string; that beats every storage API when a link should reproduce the view.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sessionStorage&lt;/code&gt; = one tab, one origin, until the tab closes. It survives reloads, SPA and full-page navigations, back/forward, and redirect round-trips; it does &lt;strong&gt;not&lt;/strong&gt; cross into new tabs (except as a one-time copy via &lt;code&gt;window.open()&lt;/code&gt; / duplicate-tab), and browsers may restore it when a closed tab is reopened.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://reactuse.com/state/usesessionstorage/" rel="noopener noreferrer"&gt;&lt;code&gt;useSessionStorage(key, default)&lt;/code&gt;&lt;/a&gt; is a drop-in &lt;code&gt;useState&lt;/code&gt; with that lifetime: same tuple, functional updates, automatic serialization for objects/Maps/Sets/Dates, &lt;code&gt;setValue(null)&lt;/code&gt; to remove, &lt;code&gt;onError&lt;/code&gt; for corrupt data and blocked storage, SSR-safe via &lt;code&gt;useSyncExternalStore&lt;/code&gt;, and every component on the same key stays in sync.&lt;/li&gt;
&lt;li&gt;Rule of thumb: two tabs disagreeing would be a bug → &lt;code&gt;localStorage&lt;/code&gt;; two tabs &lt;em&gt;agreeing&lt;/em&gt; would be a bug → &lt;code&gt;sessionStorage&lt;/code&gt;. Multi-step forms, redirect round-trips, per-tab view state, once-per-session flags, and per-tab IDs are session values.&lt;/li&gt;
&lt;li&gt;It's a lifetime, not a security boundary. Keep secrets in &lt;code&gt;httpOnly&lt;/code&gt; cookies, and clear sensitive keys yourself with &lt;code&gt;setValue(null)&lt;/code&gt; rather than trusting tab close.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;useSessionStorage&lt;/code&gt;, &lt;code&gt;useLocalStorage&lt;/code&gt;, &lt;code&gt;useCookie&lt;/code&gt;, and 110+ other SSR-safe, TypeScript-first hooks live in &lt;a href="https://reactuse.com" rel="noopener noreferrer"&gt;&lt;code&gt;@reactuses/core&lt;/code&gt;&lt;/a&gt; — one install, tree-shakeable, no dependencies to babysit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @reactuses/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>React useLatest Hook: Read Fresh State in Async Callbacks (2026)</title>
      <dc:creator>ReactUse</dc:creator>
      <pubDate>Sun, 16 Aug 2026 02:38:31 +0000</pubDate>
      <link>https://dev.to/childrentime/react-uselatest-hook-read-fresh-state-in-async-callbacks-2026-24hb</link>
      <guid>https://dev.to/childrentime/react-uselatest-hook-read-fresh-state-in-async-callbacks-2026-24hb</guid>
      <description>&lt;p&gt;Here's an autosave button that lies to the user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Editor&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;docId&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;docId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setText&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;saving&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;saved&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dirty&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;saving&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;docId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;saved&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ⚠️ but the user kept typing during the await…&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="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;textarea&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&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="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;save&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Save&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;em&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;em&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;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;The request takes 800 ms. The user types three more words while it's in flight. The promise resolves, &lt;code&gt;status&lt;/code&gt; flips to &lt;code&gt;"saved"&lt;/code&gt;, and the three new words are not saved. Inside &lt;code&gt;save&lt;/code&gt;, &lt;code&gt;text&lt;/code&gt; is whatever it was when the button was clicked — a JavaScript closure captured that render's value, and no amount of re-rendering will update it. To decide between &lt;code&gt;"saved"&lt;/code&gt; and &lt;code&gt;"dirty"&lt;/code&gt; after the &lt;code&gt;await&lt;/code&gt;, you need to know what &lt;code&gt;text&lt;/code&gt; is &lt;em&gt;now&lt;/em&gt;, and the closure can't tell you.&lt;/p&gt;

&lt;p&gt;That's the &lt;strong&gt;stale closure&lt;/strong&gt;, and it shows up anywhere a callback outlives the render that created it: &lt;code&gt;setTimeout&lt;/code&gt;, &lt;code&gt;setInterval&lt;/code&gt;, code after an &lt;code&gt;await&lt;/code&gt;, event listeners registered once, &lt;code&gt;IntersectionObserver&lt;/code&gt; and &lt;code&gt;ResizeObserver&lt;/code&gt; callbacks, WebSocket &lt;code&gt;onmessage&lt;/code&gt;, and every third-party SDK that takes a callback at construction time. React's own FAQ answers &lt;em&gt;"why am I seeing stale props or state inside my function?"&lt;/em&gt; with a ref that always holds the latest value. &lt;a href="https://reactuse.com/state/uselatest/" rel="noopener noreferrer"&gt;&lt;code&gt;useLatest&lt;/code&gt;&lt;/a&gt; from &lt;a href="https://reactuse.com" rel="noopener noreferrer"&gt;&lt;code&gt;@reactuses/core&lt;/code&gt;&lt;/a&gt; is that ref, packaged: five lines, no re-renders, no dependency arrays. This post covers what it is, why the implementation writes the ref in a layout effect rather than during render, how it relates to &lt;code&gt;useRef&lt;/code&gt;, &lt;a href="https://reactuse.com/effect/useevent/" rel="noopener noreferrer"&gt;&lt;code&gt;useEvent&lt;/code&gt;&lt;/a&gt; and React's &lt;code&gt;useEffectEvent&lt;/code&gt;, the patterns it's built for, and the one rule you must respect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @reactuses/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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;useLatest&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@reactuses/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Editor&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;docId&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;docId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setText&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;saving&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;saved&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dirty&lt;/span&gt;&lt;span class="dl"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idle&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;latestText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useLatest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;save&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;snapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// the closure: what we're sending&lt;/span&gt;
    &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;saving&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;docId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// the ref: what the user has NOW&lt;/span&gt;
    &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;latestText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;snapshot&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;saved&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dirty&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;// …&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;useLatest(value)&lt;/code&gt; returns a &lt;code&gt;MutableRefObject&amp;lt;T&amp;gt;&lt;/code&gt; whose &lt;code&gt;.current&lt;/code&gt; is always the most recently rendered &lt;code&gt;value&lt;/code&gt;. The ref object itself never changes identity, so it's safe to close over anywhere — timers, promises, subscriptions — and read whenever the callback finally runs. Notice the fixed example uses &lt;em&gt;both&lt;/em&gt; the closure and the ref: the closure is the value at the moment of the click (correct for "what did we send?"), the ref is the value at the moment the promise resolves (correct for "is it still current?"). Stale closures aren't a bug in JavaScript; they're only a bug when you wanted &lt;em&gt;now&lt;/em&gt; and got &lt;em&gt;then&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What useLatest Actually Is
&lt;/h2&gt;

&lt;p&gt;The whole hook, from &lt;code&gt;@reactuses/core&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useIsomorphicLayoutEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@reactuses/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useLatest&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&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;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&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;useIsomorphicLayoutEffect&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;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;value&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="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;ref&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;That's it. A ref, created once, whose &lt;code&gt;.current&lt;/code&gt; is overwritten every time &lt;code&gt;value&lt;/code&gt; changes. Two details are worth understanding, because they're where hand-rolled versions differ.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why a layout effect, and not &lt;code&gt;ref.current = value&lt;/code&gt; in the render body?&lt;/strong&gt; Several popular implementations (react-use, ahooks) assign during render, and it works almost all the time. But React's rules say render must be pure — no reads or writes to &lt;code&gt;ref.current&lt;/code&gt; during rendering — because with concurrent rendering a render can be started, paused, and &lt;em&gt;thrown away&lt;/em&gt; without ever committing. A ref written during a discarded render now holds a value that no committed UI ever showed. Writing in &lt;code&gt;useLayoutEffect&lt;/code&gt; means the ref updates exactly once per &lt;strong&gt;committed&lt;/strong&gt; render, synchronously after the DOM is updated and before the browser paints. That's the same trick the &lt;a href="https://github.com/reactjs/rfcs/blob/main/text/0000-useevent.md" rel="noopener noreferrer"&gt;React &lt;code&gt;useEvent&lt;/code&gt; RFC&lt;/a&gt; uses, and it's why &lt;code&gt;@reactuses/core&lt;/code&gt; builds &lt;code&gt;useEvent&lt;/code&gt;, &lt;code&gt;useInterval&lt;/code&gt;, &lt;code&gt;useTimeoutFn&lt;/code&gt; and a dozen other hooks on top of &lt;code&gt;useLatest&lt;/code&gt; instead of a bare render-time assignment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not a plain &lt;code&gt;useEffect&lt;/code&gt;?&lt;/strong&gt; Ordering. Passive effects run after paint, and in the same commit React runs children's effects before parents' and earlier hooks before later ones. If some &lt;em&gt;other&lt;/em&gt; effect in the same commit reads the ref before the updating effect has run, it sees last render's value. Layout effects run before any passive effect, so by the time any &lt;code&gt;useEffect&lt;/code&gt;, event handler, timer or promise callback fires, &lt;code&gt;ref.current&lt;/code&gt; is current. (&lt;code&gt;useIsomorphicLayoutEffect&lt;/code&gt; is just &lt;code&gt;useLayoutEffect&lt;/code&gt; in the browser and &lt;code&gt;useEffect&lt;/code&gt; on the server, so there's no SSR warning.)&lt;/p&gt;

&lt;p&gt;The consequence you should internalize: &lt;strong&gt;&lt;code&gt;.current&lt;/code&gt; reflects the last committed render, and it's meant to be read from callbacks, not from render.&lt;/strong&gt; During the render of update N+1, &lt;code&gt;ref.current&lt;/code&gt; still holds N's value — which is fine, because in render you should be reading &lt;code&gt;value&lt;/code&gt; directly anyway. If you find yourself writing &lt;code&gt;{latest.current}&lt;/code&gt; in JSX, you wanted plain state.&lt;/p&gt;

&lt;h2&gt;
  
  
  useLatest vs useRef vs useState
&lt;/h2&gt;

&lt;p&gt;These three get confused because they all "hold a value". The question is &lt;em&gt;who&lt;/em&gt; needs the value and &lt;em&gt;when&lt;/em&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You need to…&lt;/th&gt;
&lt;th&gt;Reach for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;render the value, and re-render when it changes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;useState&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;keep a mutable value across renders that &lt;strong&gt;isn't&lt;/strong&gt; derived from a prop/state (a timer id, a DOM node, a counter)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;useRef&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;read the &lt;strong&gt;latest&lt;/strong&gt; prop or state from a callback that outlives the render&lt;/td&gt;
&lt;td&gt;&lt;code&gt;useLatest&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;useLatest&lt;/code&gt; is &lt;code&gt;useRef&lt;/code&gt; plus the "keep me in sync" effect. If you've ever written this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;textRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;textRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&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="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…that's &lt;code&gt;useLatest(text)&lt;/code&gt;, minus the layout-effect timing detail above. It's also strictly more honest than the &lt;em&gt;other&lt;/em&gt; common workaround — copying state into a ref inside the setter (&lt;code&gt;setText(v); textRef.current = v;&lt;/code&gt;) — which silently breaks the moment anything else updates &lt;code&gt;text&lt;/code&gt; (a reset button, a prop, a form library).&lt;/p&gt;

&lt;h2&gt;
  
  
  useLatest vs useEvent vs useEffectEvent
&lt;/h2&gt;

&lt;p&gt;Now the neighbours. All three exist to defeat stale closures; they differ in what they wrap.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;useLatest(value)&lt;/code&gt;&lt;/strong&gt; wraps a &lt;strong&gt;value&lt;/strong&gt; and gives you a ref. You read &lt;code&gt;.current&lt;/code&gt; inside whatever callback you already have.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://reactuse.com/effect/useevent/" rel="noopener noreferrer"&gt;&lt;code&gt;useEvent(fn)&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; wraps a &lt;strong&gt;function&lt;/strong&gt; and gives you a stable function that always calls the latest &lt;code&gt;fn&lt;/code&gt;. Internally it's &lt;code&gt;useLatest(fn)&lt;/code&gt; plus &lt;code&gt;useCallback(() =&amp;gt; ref.current(...args), [])&lt;/code&gt;. Use it when the &lt;em&gt;callback itself&lt;/em&gt; is what you hand to a child, an effect, or a subscription and you want its identity to never change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;useEffectEvent&lt;/code&gt;&lt;/strong&gt; (React 19.2+) is the built-in version of &lt;code&gt;useEvent&lt;/code&gt;, restricted to being called from effects — the returned function is not stable and must not be passed as a prop or added to dependency arrays.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule of thumb: &lt;strong&gt;if you're wrapping a function, use &lt;code&gt;useEvent&lt;/code&gt;; if you're wrapping a value, use &lt;code&gt;useLatest&lt;/code&gt;.&lt;/strong&gt; They compose — the classic "subscribe once, react to fresh state" is often one &lt;code&gt;useEvent&lt;/code&gt; for the handler, or one &lt;code&gt;useLatest&lt;/code&gt; per value it reads, and either is fine. Where &lt;code&gt;useLatest&lt;/code&gt; wins outright is when the callback isn't yours to wrap: an SDK's &lt;code&gt;onChange&lt;/code&gt;, a promise continuation, an &lt;code&gt;Observer&lt;/code&gt; you construct once.&lt;/p&gt;

&lt;h2&gt;
  
  
  Patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  After an &lt;code&gt;await&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The intro's autosave is the shape: any handler that awaits and then needs to know whether the world moved on. A second common variant is the &lt;strong&gt;request race&lt;/strong&gt; in a handler rather than an effect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Search&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setQuery&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setResults&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Item&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="nx"&gt;latestQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useLatest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ChangeEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&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;setQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&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;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;q&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;latestQuery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;q&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="c1"&gt;// a newer keystroke won — drop this response&lt;/span&gt;
    &lt;span class="nf"&gt;setResults&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onChange&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside a &lt;code&gt;useEffect&lt;/code&gt; you'd use the React-docs &lt;code&gt;let ignore = false&lt;/code&gt; cleanup flag for this. Event handlers have no cleanup slot, so the ref carries the "am I still relevant?" check instead. (For debouncing the calls themselves, see &lt;a href="https://reactuse.com/effect/usedebouncefn/" rel="noopener noreferrer"&gt;&lt;code&gt;useDebounceFn&lt;/code&gt;&lt;/a&gt; — that's a different problem.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Subscriptions you create once
&lt;/h3&gt;

&lt;p&gt;Anything expensive or stateful to set up — a map, a chart, a WebSocket, a &lt;code&gt;ResizeObserver&lt;/code&gt; — should be created once and &lt;em&gt;read&lt;/em&gt; fresh state, not be torn down and rebuilt on every keystroke:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;PinMap&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;filters&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Filters&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;container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLDivElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&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;latestFilters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useLatest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;mapboxgl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;container&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;STYLE&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;moveend&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;loadPins&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBounds&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;latestFilters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// fresh filters, map built once&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&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;// ✅ empty deps are honest here — nothing inside is stale&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without the ref, the choice is &lt;code&gt;filters&lt;/code&gt; in the deps (map destroyed and recreated on every filter change — flicker, lost viewport, re-download of tiles) or an empty deps array with a lint warning and a bug. &lt;code&gt;useLatest&lt;/code&gt; gives you a third option: the effect genuinely depends on nothing, because it reads through a ref that's always current.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timers
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;setTimeout&lt;/code&gt; and &lt;code&gt;setInterval&lt;/code&gt; are the textbook stale-closure factories:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Toast&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onDismiss&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;onDismiss&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;paused&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setPaused&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;latestPaused&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useLatest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paused&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&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="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;latestPaused&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;onDismiss&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// hovered at the 5s mark? stay open&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;onMouseEnter&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&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="nf"&gt;setPaused&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="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onMouseLeave&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&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="nf"&gt;setPaused&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For anything beyond a one-off, don't hand-roll it: &lt;a href="https://reactuse.com/effect/usetimeoutfn/" rel="noopener noreferrer"&gt;&lt;code&gt;useTimeoutFn&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://reactuse.com/effect/useinterval/" rel="noopener noreferrer"&gt;&lt;code&gt;useInterval&lt;/code&gt;&lt;/a&gt; already keep the callback fresh via &lt;code&gt;useLatest&lt;/code&gt; and add &lt;code&gt;pause&lt;/code&gt;/&lt;code&gt;resume&lt;/code&gt;/&lt;code&gt;immediate&lt;/code&gt; on top — the previous post in this series, &lt;a href="https://reactuse.com/blog/react-useinterval-hook/" rel="noopener noreferrer"&gt;React useInterval Hook&lt;/a&gt;, walks through exactly that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where @reactuses/core uses it
&lt;/h3&gt;

&lt;p&gt;If you want to see the pattern in production code, &lt;code&gt;useLatest&lt;/code&gt; is the quiet workhorse behind a large chunk of the library. &lt;a href="https://reactuse.com/effect/useeventlistener/" rel="noopener noreferrer"&gt;&lt;code&gt;useEventListener&lt;/code&gt;&lt;/a&gt; wraps your handler in it so &lt;code&gt;addEventListener&lt;/code&gt; runs once per element, not once per render. &lt;a href="https://reactuse.com/element/useclickoutside/" rel="noopener noreferrer"&gt;&lt;code&gt;useClickOutside&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://reactuse.com/element/useintersectionobserver/" rel="noopener noreferrer"&gt;&lt;code&gt;useIntersectionObserver&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://reactuse.com/element/useresizeobserver/" rel="noopener noreferrer"&gt;&lt;code&gt;useResizeObserver&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://reactuse.com/element/usemutationobserver/" rel="noopener noreferrer"&gt;&lt;code&gt;useMutationObserver&lt;/code&gt;&lt;/a&gt; all construct their observer once and call &lt;code&gt;savedCallback.current&lt;/code&gt; from inside it. &lt;a href="https://reactuse.com/effect/useraffn/" rel="noopener noreferrer"&gt;&lt;code&gt;useRafFn&lt;/code&gt;&lt;/a&gt; reads the latest frame callback without cancelling the animation loop. &lt;a href="https://reactuse.com/effect/useunmount/" rel="noopener noreferrer"&gt;&lt;code&gt;useUnmount&lt;/code&gt;&lt;/a&gt; uses it so the cleanup you passed on the &lt;em&gt;first&lt;/em&gt; render doesn't run with first-render values on unmount. Same five lines, every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas Worth Knowing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's not reactive.&lt;/strong&gt; Writing to or reading from &lt;code&gt;.current&lt;/code&gt; never triggers a re-render. If a change should show up on screen, it belongs in state — &lt;code&gt;useLatest&lt;/code&gt; is for callbacks that &lt;em&gt;read&lt;/em&gt;, not for values that &lt;em&gt;display&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't read it during render.&lt;/strong&gt; Because the ref is updated in a layout effect, during render it lags one commit behind. That's by design and never matters when you read it from callbacks; it matters instantly if you put &lt;code&gt;latest.current&lt;/code&gt; in JSX or in a &lt;code&gt;useMemo&lt;/code&gt;. Read the value directly there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't put it in a dependency array expecting it to trigger anything.&lt;/strong&gt; The ref's identity is stable for the component's lifetime, so &lt;code&gt;[latestFoo]&lt;/code&gt; is equivalent to &lt;code&gt;[]&lt;/code&gt;. That's a feature — it means the effect that reads it never re-runs because of it — but it also means you can't use it to &lt;em&gt;react&lt;/em&gt; to changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The lag window is real, and tiny.&lt;/strong&gt; Between render and the layout effect commit, &lt;code&gt;.current&lt;/code&gt; is last render's value. Nothing user-visible runs in that window (no events, no timers, no passive effects), so it's a non-issue in practice, and it's the price of never leaking a discarded render into the ref.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sometimes a restart &lt;em&gt;is&lt;/em&gt; what you want.&lt;/strong&gt; If your effect should re-run when a value changes — reconnect a socket when the &lt;code&gt;roomId&lt;/code&gt; changes — put &lt;code&gt;roomId&lt;/code&gt; in the deps like normal. Use &lt;code&gt;useLatest&lt;/code&gt; only for values the callback should read &lt;em&gt;without&lt;/em&gt; causing a restart. Mixing the two in one effect (&lt;code&gt;[roomId]&lt;/code&gt; in deps, &lt;code&gt;latestFilters.current&lt;/code&gt; inside) is completely normal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSR-safe.&lt;/strong&gt; It's a ref and an isomorphic layout effect; nothing touches &lt;code&gt;window&lt;/code&gt;, and there's no hydration mismatch because it never renders anything.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When Not to Use useLatest
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The value is displayed&lt;/strong&gt; → &lt;code&gt;useState&lt;/code&gt;, always.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're wrapping a function to hand to a child or effect&lt;/strong&gt; → &lt;a href="https://reactuse.com/effect/useevent/" rel="noopener noreferrer"&gt;&lt;code&gt;useEvent&lt;/code&gt;&lt;/a&gt; (or &lt;code&gt;useEffectEvent&lt;/code&gt; on React 19.2+ if it stays inside an effect).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want the value from the &lt;em&gt;previous&lt;/em&gt; render&lt;/strong&gt; → &lt;a href="https://reactuse.com/state/useprevious/" rel="noopener noreferrer"&gt;&lt;code&gt;usePrevious&lt;/code&gt;&lt;/a&gt; — the mirror image of &lt;code&gt;useLatest&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You want to know whether the component is still mounted before setting state after an &lt;code&gt;await&lt;/code&gt;&lt;/strong&gt; → &lt;a href="https://reactuse.com/state/usemountedstate/" rel="noopener noreferrer"&gt;&lt;code&gt;useMountedState&lt;/code&gt;&lt;/a&gt; is that exact boolean.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The "stale" value is a timer or DOM callback&lt;/strong&gt; → you probably want &lt;a href="https://reactuse.com/effect/useinterval/" rel="noopener noreferrer"&gt;&lt;code&gt;useInterval&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://reactuse.com/effect/usetimeoutfn/" rel="noopener noreferrer"&gt;&lt;code&gt;useTimeoutFn&lt;/code&gt;&lt;/a&gt; or &lt;a href="https://reactuse.com/effect/useeventlistener/" rel="noopener noreferrer"&gt;&lt;code&gt;useEventListener&lt;/code&gt;&lt;/a&gt;, which already do the &lt;code&gt;useLatest&lt;/code&gt; dance for you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A callback that outlives its render — timer, &lt;code&gt;await&lt;/code&gt;, subscription, SDK hook — sees the props and state of the render that created it. That's a closure doing its job; it's a bug only when you needed &lt;em&gt;now&lt;/em&gt; and got &lt;em&gt;then&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://reactuse.com/state/uselatest/" rel="noopener noreferrer"&gt;&lt;code&gt;useLatest&lt;/code&gt;&lt;/a&gt; is a ref kept in sync with a value in a layout effect: always current from any callback, never causes a re-render, never changes identity, never leaks a discarded render.&lt;/li&gt;
&lt;li&gt;Value → &lt;code&gt;useLatest&lt;/code&gt;. Function → &lt;a href="https://reactuse.com/effect/useevent/" rel="noopener noreferrer"&gt;&lt;code&gt;useEvent&lt;/code&gt;&lt;/a&gt;. Displayed → &lt;code&gt;useState&lt;/code&gt;. Previous render → &lt;a href="https://reactuse.com/state/useprevious/" rel="noopener noreferrer"&gt;&lt;code&gt;usePrevious&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Read &lt;code&gt;.current&lt;/code&gt; from callbacks, never from render, and keep genuine restart triggers (&lt;code&gt;roomId&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;) in your dependency array — &lt;code&gt;useLatest&lt;/code&gt; is for what the callback should read &lt;em&gt;through&lt;/em&gt;, not for what should &lt;em&gt;restart&lt;/em&gt; it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;useLatest&lt;/code&gt;, &lt;code&gt;useEvent&lt;/code&gt;, &lt;code&gt;usePrevious&lt;/code&gt;, and 110+ other SSR-safe, TypeScript-first hooks live in &lt;a href="https://reactuse.com" rel="noopener noreferrer"&gt;&lt;code&gt;@reactuses/core&lt;/code&gt;&lt;/a&gt; — one install, tree-shakeable, no dependencies to babysit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @reactuses/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>React useInterval Hook: setInterval Without Stale Closures (2026)</title>
      <dc:creator>ReactUse</dc:creator>
      <pubDate>Sat, 15 Aug 2026 08:48:09 +0000</pubDate>
      <link>https://dev.to/childrentime/react-useinterval-hook-setinterval-without-stale-closures-2026-5bla</link>
      <guid>https://dev.to/childrentime/react-useinterval-hook-setinterval-without-stale-closures-2026-5bla</guid>
      <description>&lt;p&gt;Every React developer writes this component once, and it never does what they expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&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="nf"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&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="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It goes &lt;code&gt;0&lt;/code&gt;, &lt;code&gt;1&lt;/code&gt;… and stays at &lt;code&gt;1&lt;/code&gt; forever. The interval callback was created on the first render, where &lt;code&gt;count&lt;/code&gt; was &lt;code&gt;0&lt;/code&gt;, and an empty dependency array means it never sees another render. &lt;code&gt;setCount(0 + 1)&lt;/code&gt; runs every second and nothing changes. This is the single most searched React timer bug, and the "fixes" people find — add &lt;code&gt;count&lt;/code&gt; to the deps (now the interval is torn down and recreated every second), use the functional updater (works, until the callback needs anything &lt;em&gt;other&lt;/em&gt; than the previous count) — all fight the underlying mismatch: &lt;code&gt;setInterval&lt;/code&gt; is imperative and lives outside React's render cycle, while everything it wants to read lives inside it.&lt;/p&gt;

&lt;p&gt;Dan Abramov's 2019 essay &lt;em&gt;Making setInterval Declarative with React Hooks&lt;/em&gt; gave the mismatch a proper solution: a &lt;code&gt;useInterval&lt;/code&gt; hook that keeps the &lt;em&gt;latest&lt;/em&gt; callback in a ref and never restarts the timer just because your component re-rendered. &lt;a href="https://reactuse.com/effect/useinterval/" rel="noopener noreferrer"&gt;&lt;code&gt;useInterval&lt;/code&gt;&lt;/a&gt; from &lt;a href="https://reactuse.com" rel="noopener noreferrer"&gt;&lt;code&gt;@reactuses/core&lt;/code&gt;&lt;/a&gt; is that idea, plus the pieces you end up needing in a real app — &lt;code&gt;null&lt;/code&gt; to pause, &lt;code&gt;pause()&lt;/code&gt; / &lt;code&gt;resume()&lt;/code&gt; controls, an &lt;code&gt;immediate&lt;/code&gt; option, and cleanup that survives StrictMode. This post covers how it works, the two ways to pause, dynamic polling intervals, the background-tab problem, and when a different timer hook is the right call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @reactuses/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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;useInterval&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@reactuses/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setCount&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useInterval&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;setCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&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;// reads the CURRENT count — no functional updater needed&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the broken component from the intro, fixed by swapping &lt;code&gt;useEffect&lt;/code&gt; + &lt;code&gt;setInterval&lt;/code&gt; for &lt;code&gt;useInterval&lt;/code&gt;. The callback can read any prop or state directly, the timer is created once and cleared on unmount, and there's no dependency array to get wrong.&lt;/p&gt;

&lt;p&gt;The signature is &lt;code&gt;useInterval(callback, delay, options?)&lt;/code&gt; — &lt;code&gt;delay&lt;/code&gt; in milliseconds, or &lt;code&gt;null&lt;/code&gt; to pause — and it returns &lt;code&gt;{ isActive, pause, resume }&lt;/code&gt; for the cases where you want manual control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why setInterval and React Don't Get Along
&lt;/h2&gt;

&lt;p&gt;Under the hood there are three separate problems, and the hook solves each one differently:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Stale closures.&lt;/strong&gt; &lt;code&gt;setInterval&lt;/code&gt; holds one function reference for its whole life. That function closed over one render's props and state. Every later render creates a fresh closure — which the running interval never sees.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restart-on-render.&lt;/strong&gt; The obvious fix is to make the effect depend on whatever the callback reads: &lt;code&gt;useEffect(..., [count])&lt;/code&gt;. Now the interval is &lt;em&gt;correct&lt;/em&gt;, but it is cleared and re-created on every change — the timing resets each time, and with a fast-changing dependency the tick may never fire at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lifecycle.&lt;/strong&gt; You have to clear the interval on unmount, clear it again on StrictMode's dev remount, and — the part that turns into a small state machine — decide how to &lt;em&gt;pause&lt;/em&gt; it: a second piece of state, an &lt;code&gt;if&lt;/code&gt; around &lt;code&gt;setInterval&lt;/code&gt;, and dependencies that now include the pause flag.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's what a correct hand-rolled version looks like once all three are handled — a ref for the latest callback, an effect keyed only on &lt;code&gt;delay&lt;/code&gt;, and &lt;code&gt;null&lt;/code&gt; as the pause signal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useIntervalManual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;savedCallback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useLayoutEffect&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;savedCallback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// always the latest render's closure&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;delay&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="k"&gt;return&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;id&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="nx"&gt;savedCallback&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&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="nx"&gt;delay&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;That's essentially the core of &lt;code&gt;@reactuses/core&lt;/code&gt;'s &lt;code&gt;useInterval&lt;/code&gt; — it uses &lt;a href="https://reactuse.com/state/uselatest/" rel="noopener noreferrer"&gt;&lt;code&gt;useLatest&lt;/code&gt;&lt;/a&gt; for the ref and adds controls on top. Two properties fall out of the design, and they're the ones to internalize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Changing the callback never restarts the timer.&lt;/strong&gt; Re-render as often as you like, pass inline arrow functions, read whatever state you want — the ref is updated, the interval keeps its rhythm.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changing &lt;code&gt;delay&lt;/code&gt; does restart it.&lt;/strong&gt; &lt;code&gt;delay&lt;/code&gt; is the only dependency, so &lt;code&gt;5000 → 1000&lt;/code&gt; clears the old interval and starts a fresh one. That resets the phase: the next tick is a full &lt;code&gt;delay&lt;/code&gt; away from the moment the change committed. Usually right (it's how backoff works, below), occasionally surprising if you were expecting the in-flight tick to complete.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pausing: &lt;code&gt;null&lt;/code&gt; vs. &lt;code&gt;pause()&lt;/code&gt; / &lt;code&gt;resume()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;There are two ways to stop the interval, and picking the right one keeps your component simple.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Declarative — pass &lt;code&gt;null&lt;/code&gt; as the delay.&lt;/strong&gt; When "should this be running?" is derivable from state or props, encode it in the delay expression and let the hook follow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;LivePrice&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;live&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;live&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setPrice&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="nx"&gt;live&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// false → paused, true → polling every 5s&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;—&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flip &lt;code&gt;live&lt;/code&gt; and the interval clears or restarts. No effect, no ref, no extra state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Imperative — &lt;code&gt;controls: true&lt;/code&gt; and the returned handles.&lt;/strong&gt; When starting and stopping is a &lt;em&gt;user action&lt;/em&gt; rather than a derived condition (a Start/Stop button, "pause while this modal is open"), opt out of automatic starting and drive it yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Stopwatch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;elapsed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setElapsed&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;running&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setRunning&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;pause&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resume&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setElapsed&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;startedAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// read the clock, don't count ticks&lt;/span&gt;
    &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;controls&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="c1"&gt;// don't start on mount — wait for resume()&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;toggle&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;running&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;pause&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="nx"&gt;startedAt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&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;elapsed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nf"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;setRunning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;running&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&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="si"&gt;}&lt;/span&gt;s&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;toggle&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;running&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Pause&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Start&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&amp;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;&lt;code&gt;pause&lt;/code&gt; and &lt;code&gt;resume&lt;/code&gt; have stable identities (safe in dependency arrays and event handlers), and the interval is still cleared on unmount even in &lt;code&gt;controls&lt;/code&gt; mode — you can't leak a timer by forgetting. One honest note: &lt;code&gt;isActive&lt;/code&gt; in the return value is a &lt;strong&gt;ref&lt;/strong&gt; (&lt;code&gt;isActive.current&lt;/code&gt;), not state — it won't re-render your component when it flips, which is why the example above keeps its own &lt;code&gt;running&lt;/code&gt; state for the button label.&lt;/p&gt;

&lt;p&gt;You can also mix the two: without &lt;code&gt;controls&lt;/code&gt;, &lt;code&gt;pause()&lt;/code&gt; still works as a temporary override, and the next &lt;code&gt;delay&lt;/code&gt; change resumes automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;immediate&lt;/code&gt;: Fire Now, Then Every N ms
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;setInterval&lt;/code&gt; waits a full &lt;code&gt;delay&lt;/code&gt; before its first call, which is almost never what you want for polling — the user stares at an empty screen for five seconds. &lt;code&gt;immediate: true&lt;/code&gt; runs the callback synchronously when the interval starts, then keeps the schedule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;refreshDashboard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;immediate&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that "when the interval starts" includes every &lt;code&gt;delay&lt;/code&gt; change — every time the delay value changes, the callback fires once right away and the schedule restarts. That's handy when a &lt;em&gt;user&lt;/em&gt; changes the refresh rate, but it's exactly wrong for a failure-driven backoff (each widening of the delay would trigger another call on the spot), so leave &lt;code&gt;immediate&lt;/code&gt; off there — see the next section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Polling with backoff
&lt;/h3&gt;

&lt;p&gt;Because &lt;code&gt;delay&lt;/code&gt; is a normal value, backoff is just state. On failure, widen the interval; on success, snap back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useJobStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Job&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setDelay&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &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;try&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;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;jobId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&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;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;setDelay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;            &lt;span class="c1"&gt;// stop polling&lt;/span&gt;
      &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;setDelay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                     &lt;span class="c1"&gt;// healthy → base rate&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setDelay&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// back off, cap at 1 min&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;status&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;Each &lt;code&gt;setDelay&lt;/code&gt; restarts the interval with the new cadence — and because &lt;code&gt;immediate&lt;/code&gt; is off, a failure waits the &lt;em&gt;new, longer&lt;/em&gt; delay before trying again, which is the whole point. There's no timer bookkeeping anywhere in that hook — it's all "what should the delay be right now?".&lt;/p&gt;

&lt;h3&gt;
  
  
  Pause in background tabs (and offline)
&lt;/h3&gt;

&lt;p&gt;Browsers throttle timers in hidden tabs — to roughly once per second, and Chrome drops to once per &lt;em&gt;minute&lt;/em&gt; after a tab has been hidden for five minutes. Polling in a background tab therefore both wastes quota &lt;em&gt;and&lt;/em&gt; fires at unpredictable times. The fix composes naturally with the &lt;code&gt;null&lt;/code&gt; pattern using &lt;a href="https://reactuse.com/element/usedocumentvisibility/" rel="noopener noreferrer"&gt;&lt;code&gt;useDocumentVisibility&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://reactuse.com/browser/useonline/" rel="noopener noreferrer"&gt;&lt;code&gt;useOnline&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;visible&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useDocumentVisibility&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;visible&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;online&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useOnline&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nf"&gt;useInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;visible&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;online&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&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="na"&gt;immediate&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the user comes back, &lt;code&gt;delay&lt;/code&gt; flips from &lt;code&gt;null&lt;/code&gt; to &lt;code&gt;10_000&lt;/code&gt;, the interval restarts, and &lt;code&gt;immediate&lt;/code&gt; fetches fresh data right away — exactly the "resume and catch up" behavior you'd otherwise hand-code with &lt;code&gt;visibilitychange&lt;/code&gt; listeners.&lt;/p&gt;

&lt;h3&gt;
  
  
  Clocks: schedule ticks, don't count them
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;setInterval&lt;/code&gt; drifts. Over a minute of "every 1000ms" you can lose a second or more, especially in throttled tabs. So don't accumulate time in the callback — use the interval only to trigger a re-render and read the real clock:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Clock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setNow&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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="nf"&gt;useInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setNow&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="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;time&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&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;now&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLocaleTimeString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;time&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interval is allowed to be sloppy; the displayed value is always correct because it comes from &lt;code&gt;Date.now()&lt;/code&gt;, not from &lt;code&gt;ticks × 1000&lt;/code&gt;. Same rule for elapsed-time displays: store a start timestamp, render &lt;code&gt;Date.now() - start&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Not to Use useInterval
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You want one delayed call, not a repeating one.&lt;/strong&gt; That's &lt;a href="https://reactuse.com/effect/usetimeoutfn/" rel="noopener noreferrer"&gt;&lt;code&gt;useTimeoutFn&lt;/code&gt;&lt;/a&gt; — &lt;code&gt;const [pending, start, stop] = useTimeoutFn(fn, ms)&lt;/code&gt; — or &lt;a href="https://reactuse.com/effect/usetimeout/" rel="noopener noreferrer"&gt;&lt;code&gt;useTimeout&lt;/code&gt;&lt;/a&gt; if all you need is a re-render after N ms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're building a countdown display.&lt;/strong&gt; &lt;a href="https://reactuse.com/state/usecountdown/" rel="noopener noreferrer"&gt;&lt;code&gt;useCountDown&lt;/code&gt;&lt;/a&gt; already does the seconds → &lt;code&gt;hh:mm:ss&lt;/code&gt; math and completion callback on top of &lt;code&gt;useInterval&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're animating.&lt;/strong&gt; Anything visual that should update every frame belongs in &lt;code&gt;requestAnimationFrame&lt;/code&gt;, which is what &lt;a href="https://reactuse.com/effect/useraffn/" rel="noopener noreferrer"&gt;&lt;code&gt;useRafFn&lt;/code&gt;&lt;/a&gt; wraps — it syncs to the display refresh rate and pauses automatically in hidden tabs. A 16ms &lt;code&gt;setInterval&lt;/code&gt; is not the same thing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're rate-limiting a handler, not scheduling one.&lt;/strong&gt; Firing on the &lt;em&gt;trailing edge of user input&lt;/em&gt; is &lt;a href="https://reactuse.com/effect/usedebouncefn/" rel="noopener noreferrer"&gt;&lt;code&gt;useDebounceFn&lt;/code&gt;&lt;/a&gt; / &lt;a href="https://reactuse.com/effect/usethrottlefn/" rel="noopener noreferrer"&gt;&lt;code&gt;useThrottleFn&lt;/code&gt;&lt;/a&gt; territory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The "interval" is really server push.&lt;/strong&gt; If the server can tell you when something changed, a Server-Sent Events stream via &lt;a href="https://reactuse.com/browser/useeventsource/" rel="noopener noreferrer"&gt;&lt;code&gt;useEventSource&lt;/code&gt;&lt;/a&gt; beats polling on latency and cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You want…&lt;/th&gt;
&lt;th&gt;Reach for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;run &lt;code&gt;fn&lt;/code&gt; every N ms, pause with &lt;code&gt;null&lt;/code&gt; or &lt;code&gt;pause()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;a href="https://reactuse.com/effect/useinterval/" rel="noopener noreferrer"&gt;&lt;code&gt;useInterval&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;run &lt;code&gt;fn&lt;/code&gt; once after N ms, with &lt;code&gt;start&lt;/code&gt; / &lt;code&gt;stop&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;a href="https://reactuse.com/effect/usetimeoutfn/" rel="noopener noreferrer"&gt;&lt;code&gt;useTimeoutFn&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;re-render once after N ms&lt;/td&gt;
&lt;td&gt;&lt;a href="https://reactuse.com/effect/usetimeout/" rel="noopener noreferrer"&gt;&lt;code&gt;useTimeout&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;hh:mm:ss&lt;/code&gt; countdown from N seconds&lt;/td&gt;
&lt;td&gt;&lt;a href="https://reactuse.com/state/usecountdown/" rel="noopener noreferrer"&gt;&lt;code&gt;useCountDown&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;run &lt;code&gt;fn&lt;/code&gt; every animation frame&lt;/td&gt;
&lt;td&gt;&lt;a href="https://reactuse.com/effect/useraffn/" rel="noopener noreferrer"&gt;&lt;code&gt;useRafFn&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Gotchas Worth Knowing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The callback reads the latest &lt;em&gt;committed&lt;/em&gt; render.&lt;/strong&gt; The ref is updated in a layout effect after each render, so a tick that fires mid-render sees the previous committed values — a non-issue in practice, but the reason the hook can't be "more current than React".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;delay&lt;/code&gt; change = phase reset.&lt;/strong&gt; Covered above; if you need to change the cadence &lt;em&gt;without&lt;/em&gt; dropping the in-flight tick, keep the interval fixed and skip ticks in the callback instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;immediate&lt;/code&gt; fires inside the effect, on mount and on every &lt;code&gt;delay&lt;/code&gt; change.&lt;/strong&gt; Under React 18+ StrictMode in dev, that means the immediate call happens twice on mount (mount → cleanup → mount). Make it idempotent, as with any effect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;async&lt;/code&gt; callbacks are fine — but overlap is on you.&lt;/strong&gt; The hook doesn't wait for a returned Promise. If a fetch can take longer than &lt;code&gt;delay&lt;/code&gt;, guard with an in-flight flag or use &lt;code&gt;null&lt;/code&gt; to pause while a request is pending.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSR-safe by construction.&lt;/strong&gt; The timer is created inside an effect, so nothing runs on the server and there's no &lt;code&gt;window&lt;/code&gt; access to guard.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The stuck-at-&lt;code&gt;1&lt;/code&gt; counter is a stale-closure bug: &lt;code&gt;setInterval&lt;/code&gt; keeps the first render's callback. &lt;a href="https://reactuse.com/effect/useinterval/" rel="noopener noreferrer"&gt;&lt;code&gt;useInterval&lt;/code&gt;&lt;/a&gt; stores the latest callback in a ref, so the timer runs once and always sees current state.&lt;/li&gt;
&lt;li&gt;Only &lt;code&gt;delay&lt;/code&gt; restarts the interval — pass &lt;code&gt;null&lt;/code&gt; to pause declaratively, or &lt;code&gt;controls: true&lt;/code&gt; with &lt;code&gt;pause()&lt;/code&gt; / &lt;code&gt;resume()&lt;/code&gt; for user-driven start/stop.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;immediate: true&lt;/code&gt; fires now-then-every-N; backoff is just &lt;code&gt;setDelay(...)&lt;/code&gt;; pause polling in hidden or offline tabs by folding &lt;a href="https://reactuse.com/element/usedocumentvisibility/" rel="noopener noreferrer"&gt;&lt;code&gt;useDocumentVisibility&lt;/code&gt;&lt;/a&gt; / &lt;a href="https://reactuse.com/browser/useonline/" rel="noopener noreferrer"&gt;&lt;code&gt;useOnline&lt;/code&gt;&lt;/a&gt; into the delay expression.&lt;/li&gt;
&lt;li&gt;Never accumulate time in an interval — read &lt;code&gt;Date.now()&lt;/code&gt; — and reach for &lt;a href="https://reactuse.com/effect/usetimeoutfn/" rel="noopener noreferrer"&gt;&lt;code&gt;useTimeoutFn&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://reactuse.com/state/usecountdown/" rel="noopener noreferrer"&gt;&lt;code&gt;useCountDown&lt;/code&gt;&lt;/a&gt;, or &lt;a href="https://reactuse.com/effect/useraffn/" rel="noopener noreferrer"&gt;&lt;code&gt;useRafFn&lt;/code&gt;&lt;/a&gt; when the job isn't "every N ms, forever".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;useInterval&lt;/code&gt;, &lt;code&gt;useTimeoutFn&lt;/code&gt;, &lt;code&gt;useCountDown&lt;/code&gt;, and 110+ other SSR-safe, TypeScript-first hooks live in &lt;a href="https://reactuse.com" rel="noopener noreferrer"&gt;&lt;code&gt;@reactuses/core&lt;/code&gt;&lt;/a&gt; — one install, tree-shakeable, no dependencies to babysit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @reactuses/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>React useFocus Hook: Track &amp; Control Element Focus State (2026)</title>
      <dc:creator>ReactUse</dc:creator>
      <pubDate>Fri, 14 Aug 2026 04:01:41 +0000</pubDate>
      <link>https://dev.to/childrentime/react-usefocus-hook-track-control-element-focus-state-2026-5ffh</link>
      <guid>https://dev.to/childrentime/react-usefocus-hook-track-control-element-focus-state-2026-5ffh</guid>
      <description>&lt;p&gt;Focus is where interaction actually happens — the input receiving keystrokes, the button the keyboard user just tabbed to. Yet React gives you no state for it. &lt;code&gt;document.activeElement&lt;/code&gt; knows the answer but never tells you when it changes, the &lt;code&gt;autoFocus&lt;/code&gt; attribute fires once and can't be re-triggered, and reading focus for &lt;em&gt;rendering&lt;/em&gt; — show hints while editing, float a label, validate after leaving — means wiring &lt;code&gt;focus&lt;/code&gt;/&lt;code&gt;blur&lt;/code&gt; listeners by hand on every field that needs it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://reactuse.com/element/usefocus/" rel="noopener noreferrer"&gt;&lt;code&gt;useFocus&lt;/code&gt;&lt;/a&gt; from &lt;a href="https://reactuse.com" rel="noopener noreferrer"&gt;&lt;code&gt;@reactuses/core&lt;/code&gt;&lt;/a&gt; collapses all of that into one line: a live &lt;code&gt;isFocused&lt;/code&gt; boolean your component just renders, plus a setter that focuses or blurs the element whenever your logic decides to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @reactuses/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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;useRef&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useFocus&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@reactuses/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SearchField&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;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isFocused&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setFocused&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useFocus&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;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isFocused&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;field field--active&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;field&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Search hooks…"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isFocused&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;kbd&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"hint"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;esc to clear&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;kbd&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setFocused&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="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Jump to search&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole integration. The hook subscribes to the element's &lt;code&gt;focus&lt;/code&gt; and &lt;code&gt;blur&lt;/code&gt; events and mirrors them into state; &lt;code&gt;setFocused(true)&lt;/code&gt; calls &lt;code&gt;element.focus()&lt;/code&gt;, &lt;code&gt;setFocused(false)&lt;/code&gt; calls &lt;code&gt;element.blur()&lt;/code&gt;. One tuple, both directions — observe focus and command it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not autoFocus, activeElement, or Plain CSS?
&lt;/h2&gt;

&lt;p&gt;Each of the built-in options covers a sliver of the problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CSS &lt;code&gt;:focus&lt;/code&gt; / &lt;code&gt;:focus-within&lt;/code&gt;&lt;/strong&gt; is the right tool when the response is &lt;em&gt;pure styling&lt;/em&gt; — a border color, a glow. Use it; it costs zero JavaScript and zero re-renders. The hook earns its place the moment focus drives &lt;strong&gt;logic or JSX&lt;/strong&gt;: rendering a hints panel, deciding &lt;em&gt;when&lt;/em&gt; to validate, pausing a ticker while the user types.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;document.activeElement&lt;/code&gt;&lt;/strong&gt; is a snapshot, not a subscription. Read it in render and it's stale by the next tab-press; nothing re-renders your component when focus moves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;autoFocus&lt;/code&gt;&lt;/strong&gt; fires once, at mount, and that's the entire API. It can't focus on demand ("press &lt;code&gt;/&lt;/code&gt; to search"), can't blur, and tells you nothing about the current state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ref.current.focus()&lt;/code&gt; sprinkled in handlers&lt;/strong&gt; works — until you also need to &lt;em&gt;know&lt;/em&gt; whether the element is focused, and now you're maintaining listeners anyway.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Manual Way — and Where It Bites
&lt;/h2&gt;

&lt;p&gt;The hand-rolled version looks harmless:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ⚠️ hand-rolled — works in the demo, leaks bugs in the app&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SearchField&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;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isFocused&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsFocused&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;el&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onFocus&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="nf"&gt;setIsFocused&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onBlur&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="nf"&gt;setIsFocused&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;el&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="s2"&gt;focus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onFocus&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;el&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="s2"&gt;blur&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onBlur&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;el&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="s2"&gt;focus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onFocus&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;el&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="s2"&gt;blur&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onBlur&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="p"&gt;[]);&lt;/span&gt;
  &lt;span class="c1"&gt;// …&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three problems hiding in fifteen lines:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Late-mounting targets never get wired.&lt;/strong&gt; The &lt;code&gt;if (!el) return&lt;/code&gt; guard runs once. If the input renders conditionally — inside a modal, behind a tab, after a loading state — the effect has already returned and no listener ever attaches. An empty dependency array can't express "re-run when the element appears."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It misses the initial state.&lt;/strong&gt; If something focuses the element before your effect runs (an &lt;code&gt;autoFocus&lt;/code&gt; attribute, a router's focus restoration), your state says &lt;code&gt;false&lt;/code&gt; while the element sits there focused. You need a &lt;code&gt;document.activeElement&lt;/code&gt; check on mount &lt;em&gt;in addition to&lt;/em&gt; the listeners.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's per-field boilerplate.&lt;/strong&gt; Multiply those fifteen lines by every input in a form and the form file is mostly plumbing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;useFocus&lt;/code&gt; absorbs all three: targets can be lazy getters (&lt;code&gt;() =&amp;gt; document.querySelector(".modal input")&lt;/code&gt;) that re-resolve as the DOM changes, mount-time state is reconciled for you, and each field is one line.&lt;/p&gt;

&lt;h2&gt;
  
  
  The useFocus API
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isFocused&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setFocused&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useFocus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;initialValue&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;&lt;code&gt;target&lt;/code&gt;&lt;/strong&gt; is flexible — pass whichever you have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useFocus&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="c1"&gt;// a ref object&lt;/span&gt;
&lt;span class="nf"&gt;useFocus&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;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;search&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;       &lt;span class="c1"&gt;// an element&lt;/span&gt;
&lt;span class="nf"&gt;useFocus&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.otp input&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// a lazy getter&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SVG elements work too — the target type is &lt;code&gt;HTMLElement | SVGElement&lt;/code&gt;, so a focusable &lt;code&gt;&amp;lt;circle tabindex="0"&amp;gt;&lt;/code&gt; in a chart is fair game.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;initialValue&lt;/code&gt;&lt;/strong&gt; (default &lt;code&gt;false&lt;/code&gt;) is declarative autofocus: pass &lt;code&gt;true&lt;/code&gt; and the hook focuses the element on mount. Unlike the &lt;code&gt;autoFocus&lt;/code&gt; attribute, it goes through the same code path as &lt;code&gt;setFocused&lt;/code&gt;, works with getter targets, and leaves you holding the live state afterwards.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;setFocused&lt;/code&gt;&lt;/strong&gt; is imperative control with cleanup included: &lt;code&gt;true&lt;/code&gt; → &lt;code&gt;element.focus()&lt;/code&gt;, &lt;code&gt;false&lt;/code&gt; → &lt;code&gt;element.blur()&lt;/code&gt;. If the target doesn't exist yet, the call is a safe no-op instead of a crash.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real Patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Floating labels — the label knows when to float
&lt;/h3&gt;

&lt;p&gt;The material-style input: label sits inside the field, floats up when the field is active &lt;em&gt;or&lt;/em&gt; has content.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;FloatingLabelInput&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isFocused&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useFocus&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="kd"&gt;const&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="nx"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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;floated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;isFocused&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"float-field"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;floated&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;float-label up&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;float-label&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&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="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;label&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CSS alone gets close with &lt;code&gt;:focus-within&lt;/code&gt; + &lt;code&gt;:placeholder-shown&lt;/code&gt;, but the moment the float condition involves app state — a controlled value, a validation flag — you need focus &lt;em&gt;as state&lt;/em&gt;, and this is it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Validate on blur, not on keystroke
&lt;/h3&gt;

&lt;p&gt;Yelling "invalid email" at someone who has typed three characters is the classic form-UX failure. The fix is &lt;em&gt;touched&lt;/em&gt; semantics — validate only after the user leaves the field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;EmailField&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;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isFocused&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useFocus&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="kd"&gt;const&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="nx"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;touched&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setTouched&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isFocused&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="c1"&gt;// still editing — stay quiet&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;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;setTouched&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="c1"&gt;// left the field with content → judge it&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isFocused&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;touched&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;isFocused&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;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&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="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;That doesn't look like an email.&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;isFocused&lt;/code&gt; transition &lt;em&gt;is&lt;/em&gt; the touched signal — no &lt;code&gt;onBlur&lt;/code&gt; prop threading, and the error clears itself the moment the user comes back to fix it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Press &lt;code&gt;/&lt;/code&gt; to search
&lt;/h3&gt;

&lt;p&gt;Every documentation site does this, and &lt;code&gt;setFocused&lt;/code&gt; plus &lt;a href="https://reactuse.com/effect/useeventlistener/" rel="noopener noreferrer"&gt;&lt;code&gt;useEventListener&lt;/code&gt;&lt;/a&gt; is the entire implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;DocSearch&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;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isFocused&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setFocused&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useFocus&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="nf"&gt;useEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;keydown&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="nx"&gt;e&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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isFocused&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// don't type the slash&lt;/span&gt;
      &lt;span class="nf"&gt;setFocused&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Escape&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;setFocused&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Press / to search"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note how both halves of the tuple earn their keep: &lt;code&gt;isFocused&lt;/code&gt; guards against hijacking a &lt;code&gt;/&lt;/code&gt; the user is legitimately typing &lt;em&gt;into the field&lt;/em&gt;, and &lt;code&gt;setFocused&lt;/code&gt; does the jump.&lt;/p&gt;

&lt;h3&gt;
  
  
  Autofocus that survives conditional rendering
&lt;/h3&gt;

&lt;p&gt;Focusing the first field of a modal form, where the input doesn't exist until &lt;a href="https://reactuse.com/state/usedisclosure/" rel="noopener noreferrer"&gt;&lt;code&gt;useDisclosure&lt;/code&gt;&lt;/a&gt; says so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RenameDialog&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;open&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;open&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;useFocus&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="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// focuses on mount — which is when the dialog opens&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;open&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;dialog&lt;/span&gt; &lt;span class="na"&gt;open&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"untitled.md"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;dialog&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the component (and the hook) mounts when the dialog opens, &lt;code&gt;initialValue: true&lt;/code&gt; fires at exactly the right moment — no &lt;code&gt;setTimeout(…, 0)&lt;/code&gt; incantations.&lt;/p&gt;

&lt;h2&gt;
  
  
  useFocus vs Its Siblings
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@reactuses/core&lt;/code&gt; ships three focus hooks at three zoom levels — pick by the question you're asking:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hook&lt;/th&gt;
&lt;th&gt;Answers&lt;/th&gt;
&lt;th&gt;Reach for it when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://reactuse.com/element/usefocus/" rel="noopener noreferrer"&gt;&lt;code&gt;useFocus&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;"is &lt;strong&gt;this element&lt;/strong&gt; focused?" + control&lt;/td&gt;
&lt;td&gt;per-field UI: labels, hints, validation timing, shortcuts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://reactuse.com/element/useactiveelement/" rel="noopener noreferrer"&gt;&lt;code&gt;useActiveElement&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;"&lt;strong&gt;which element&lt;/strong&gt; has focus, document-wide?"&lt;/td&gt;
&lt;td&gt;form-level logic, focus debugging, roving-focus widgets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://reactuse.com/element/usewindowfocus/" rel="noopener noreferrer"&gt;&lt;code&gt;useWindowFocus&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;"is the &lt;strong&gt;tab/window&lt;/strong&gt; focused at all?"&lt;/td&gt;
&lt;td&gt;pausing polling or animations when the user switches away&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CSS &lt;code&gt;:focus&lt;/code&gt; / &lt;code&gt;:focus-visible&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;styling only&lt;/td&gt;
&lt;td&gt;any pure-CSS response — always try this first&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rule of thumb: one element → &lt;code&gt;useFocus&lt;/code&gt;; whole document → &lt;a href="https://reactuse.com/element/useactiveelement/" rel="noopener noreferrer"&gt;&lt;code&gt;useActiveElement&lt;/code&gt;&lt;/a&gt;; the browser window itself → &lt;a href="https://reactuse.com/element/usewindowfocus/" rel="noopener noreferrer"&gt;&lt;code&gt;useWindowFocus&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SSR is handled.&lt;/strong&gt; There's no DOM on the server; the hook renders the &lt;code&gt;initialValue&lt;/code&gt; you gave it and wires listeners after hydration — no &lt;code&gt;typeof window&lt;/code&gt; guards in your code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;element.focus()&lt;/code&gt; scrolls.&lt;/strong&gt; Browsers scroll a newly focused element into view. Autofocusing something below the fold on page load yanks the viewport — reserve &lt;code&gt;initialValue: true&lt;/code&gt; for elements that are already where the user is looking (modals, inline editors).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't steal focus.&lt;/strong&gt; Moving focus is an accessibility action, not a visual one: screen readers announce the newly focused element, and keyboard users lose their place. Focus in response to &lt;em&gt;user intent&lt;/em&gt; (a shortcut, opening a dialog), never on a timer or a data refresh.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blur sends focus to &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;setFocused(false)&lt;/code&gt; doesn't restore focus to where it was before — after closing a dialog, hand focus back to the trigger button explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Style with &lt;code&gt;:focus-visible&lt;/code&gt;, decide with &lt;code&gt;isFocused&lt;/code&gt;.&lt;/strong&gt; Keyboard-only focus rings are a solved CSS problem; keep the ring in CSS and spend the hook's state on logic. Related concerns compose the same way — clicks outside the field are &lt;a href="https://reactuse.com/element/useclickoutside/" rel="noopener noreferrer"&gt;&lt;code&gt;useClickOutside&lt;/code&gt;&lt;/a&gt;, not a blur hack.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;React has no focus state, and the primitives don't compose into one: &lt;code&gt;document.activeElement&lt;/code&gt; isn't reactive, &lt;code&gt;autoFocus&lt;/code&gt; is fire-once, and hand-rolled listeners miss late-mounting elements and pre-focused mounts. &lt;a href="https://reactuse.com/element/usefocus/" rel="noopener noreferrer"&gt;&lt;code&gt;useFocus&lt;/code&gt;&lt;/a&gt; is the missing &lt;code&gt;[isFocused, setFocused]&lt;/code&gt; tuple.&lt;/li&gt;
&lt;li&gt;The setter is bidirectional control — &lt;code&gt;true&lt;/code&gt; focuses, &lt;code&gt;false&lt;/code&gt; blurs, safely no-oping if the element isn't there yet; &lt;code&gt;initialValue: true&lt;/code&gt; is declarative autofocus that lands exactly at mount.&lt;/li&gt;
&lt;li&gt;The killer patterns are timing patterns: float labels while editing, validate only after leaving, jump to search on &lt;code&gt;/&lt;/code&gt;, focus the modal's first field the instant it exists.&lt;/li&gt;
&lt;li&gt;Pure styling belongs to CSS &lt;code&gt;:focus&lt;/code&gt; and &lt;code&gt;:focus-visible&lt;/code&gt; — spend the hook on logic and JSX. And pick your zoom level: element → &lt;code&gt;useFocus&lt;/code&gt;, document → &lt;a href="https://reactuse.com/element/useactiveelement/" rel="noopener noreferrer"&gt;&lt;code&gt;useActiveElement&lt;/code&gt;&lt;/a&gt;, window → &lt;a href="https://reactuse.com/element/usewindowfocus/" rel="noopener noreferrer"&gt;&lt;code&gt;useWindowFocus&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Focus is an accessibility surface: move it on user intent, never steal it, and return it where it came from when you're done.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;useFocus&lt;/code&gt; and 110+ other SSR-safe, TypeScript-first hooks live in &lt;a href="https://reactuse.com" rel="noopener noreferrer"&gt;&lt;code&gt;@reactuses/core&lt;/code&gt;&lt;/a&gt; — one install, tree-shakeable, no dependencies to babysit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @reactuses/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>React useElementSize Hook: Track Element Width &amp; Height with ResizeObserver (2026)</title>
      <dc:creator>ReactUse</dc:creator>
      <pubDate>Thu, 13 Aug 2026 02:15:02 +0000</pubDate>
      <link>https://dev.to/childrentime/react-useelementsize-hook-track-element-width-height-with-resizeobserver-2026-2gan</link>
      <guid>https://dev.to/childrentime/react-useelementsize-hook-track-element-width-height-with-resizeobserver-2026-2gan</guid>
      <description>&lt;p&gt;Media queries answer one question: &lt;em&gt;how big is the viewport?&lt;/em&gt; But your components don't live in the viewport — they live in columns, cards, panels, and grid tracks. The same &lt;code&gt;&amp;lt;ProductCard&amp;gt;&lt;/code&gt; is 900px wide in a full-width main column and 320px wide next to an open sidebar, on the &lt;em&gt;same screen&lt;/em&gt;. And an element's size changes for a dozen reasons that never fire a window &lt;code&gt;resize&lt;/code&gt; event: a sidebar collapses, an accordion expands, a font finishes loading, a flex sibling appears, content streams in.&lt;/p&gt;

&lt;p&gt;Tracking any of that means &lt;code&gt;ResizeObserver&lt;/code&gt; — the browser API built for exactly this — wrapped in the usual React ceremony of refs, effects, and cleanup. &lt;a href="https://reactuse.com/element/useelementsize/" rel="noopener noreferrer"&gt;&lt;code&gt;useElementSize&lt;/code&gt;&lt;/a&gt; from &lt;a href="https://reactuse.com" rel="noopener noreferrer"&gt;&lt;code&gt;@reactuses/core&lt;/code&gt;&lt;/a&gt; reduces the whole thing to two numbers your component just renders: &lt;code&gt;[width, height]&lt;/code&gt;, live, for any element.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @reactuses/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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;useRef&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useElementSize&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@reactuses/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ChartCard&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;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLDivElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useElementSize&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;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"chart-card"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Chart&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire integration. The hook attaches a &lt;code&gt;ResizeObserver&lt;/code&gt; on mount, and because &lt;code&gt;ResizeObserver&lt;/code&gt; reports once immediately on &lt;code&gt;observe()&lt;/code&gt;, &lt;code&gt;width&lt;/code&gt; and &lt;code&gt;height&lt;/code&gt; populate right after first paint without a separate "measure on mount" pass. Every subsequent size change — window resize, sidebar toggle, content reflow — updates the state. Unmount cleans up the observer. No refs to observers, no &lt;code&gt;disconnect()&lt;/code&gt; to forget.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not window.innerWidth or a Media Query?
&lt;/h2&gt;

&lt;p&gt;Because most element resizes have nothing to do with the window:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A collapsible sidebar opens and your main column loses 280px — viewport unchanged.&lt;/li&gt;
&lt;li&gt;A user drags a split-pane divider.&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; above your element finishes loading and pushes everything down and reflows the column.&lt;/li&gt;
&lt;li&gt;A filter empties a flex row and the survivors stretch.&lt;/li&gt;
&lt;li&gt;A CSS transition animates a panel's width over 300ms.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Window-level tools are blind to all of it. &lt;a href="https://reactuse.com/element/usewindowsize/" rel="noopener noreferrer"&gt;&lt;code&gt;useWindowSize&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://reactuse.com/browser/usemediaquery/" rel="noopener noreferrer"&gt;&lt;code&gt;useMediaQuery&lt;/code&gt;&lt;/a&gt; are the right calls for &lt;em&gt;page-level&lt;/em&gt; layout decisions — but a component that keys its layout on viewport width breaks the first time someone renders it in a narrow column on a wide screen.&lt;/p&gt;

&lt;p&gt;CSS container queries deserve a mention here: if your response to size is &lt;em&gt;pure styling&lt;/em&gt;, &lt;code&gt;@container&lt;/code&gt; handles it with zero JavaScript and zero re-renders — use that. The hook earns its place the moment you need the number &lt;strong&gt;in JS&lt;/strong&gt;: chart dimensions, canvas backing stores, virtualization math, or rendering a genuinely different component tree.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Manual Way — and Where It Bites
&lt;/h2&gt;

&lt;p&gt;Hand-rolling looks short enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ⚠️ hand-rolled — works in the demo, leaks bugs in the app&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ChartCard&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;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLDivElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSize&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;width&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="na"&gt;height&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="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="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;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ro&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;ResizeObserver&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;entry&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;setSize&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&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="nx"&gt;ro&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ro&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;…&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three problems hiding in ten lines:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Late-mounting targets never get observed.&lt;/strong&gt; That &lt;code&gt;if (!ref.current) return&lt;/code&gt; guard runs once, on mount. If the element renders conditionally — behind a loading state, a tab, a modal — the effect already returned and nothing ever attaches. You need the effect to re-run when the &lt;em&gt;element&lt;/em&gt; appears, which an empty dependency array can't express.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The options-identity trap.&lt;/strong&gt; Want &lt;code&gt;{ box: "border-box" }&lt;/code&gt;? That object literal is new every render. Put it in the effect's dependency array and you tear down and recreate the observer on every render; leave it out and the lint rule yells or a later edit silently stales it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You read the wrong box.&lt;/strong&gt; &lt;code&gt;contentRect&lt;/code&gt; is the legacy field, kept for compatibility. The modern fields — &lt;code&gt;borderBoxSize&lt;/code&gt;, &lt;code&gt;contentBoxSize&lt;/code&gt;, &lt;code&gt;devicePixelContentBoxSize&lt;/code&gt; — are &lt;em&gt;arrays&lt;/em&gt; (elements can fragment across columns), and picking and summing the right one is more code than the observer itself.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;useElementSize&lt;/code&gt; absorbs all three: for conditionally rendered elements you pass a lazy getter (&lt;code&gt;() =&amp;gt; document.querySelector(".panel")&lt;/code&gt;) and the hook re-resolves it every render, attaching the moment the element exists; options are deep-compared (inline literals are fine); and box selection — including fragment summation — is handled per spec.&lt;/p&gt;

&lt;h2&gt;
  
  
  The useElementSize API
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useElementSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&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;&lt;code&gt;target&lt;/code&gt;&lt;/strong&gt; is flexible — pass whichever you have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useElementSize&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="c1"&gt;// a ref object&lt;/span&gt;
&lt;span class="nf"&gt;useElementSize&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;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hero&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;        &lt;span class="c1"&gt;// an element&lt;/span&gt;
&lt;span class="nf"&gt;useElementSize&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.panel&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// a lazy getter&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;options&lt;/code&gt;&lt;/strong&gt; is a standard &lt;code&gt;ResizeObserverOptions&lt;/code&gt; — one field, &lt;code&gt;box&lt;/code&gt;, three values. And because the hook deep-compares options internally, &lt;code&gt;useElementSize(ref, { box: "border-box" })&lt;/code&gt; with an inline literal does &lt;em&gt;not&lt;/em&gt; churn the observer every render.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which box should you measure?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;content-box&lt;/code&gt;&lt;/strong&gt; (default) — the content area only: padding and border excluded. This is "how much room does my &lt;em&gt;content&lt;/em&gt; have," the right box for laying out children, charts, and column math.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;border-box&lt;/code&gt;&lt;/strong&gt; — padding and border included; matches &lt;code&gt;offsetWidth&lt;/code&gt;/&lt;code&gt;offsetHeight&lt;/code&gt; and how much space the element occupies in layout. Reach for it when coordinating with siblings or overlays.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;device-pixel-content-box&lt;/code&gt;&lt;/strong&gt; — the content box in &lt;strong&gt;physical device pixels&lt;/strong&gt;. This one is special.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The crisp-canvas trick
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; whose backing store doesn't match its physical pixel size renders blurry on 2× displays. The folk fix — multiply CSS pixels by &lt;code&gt;devicePixelRatio&lt;/code&gt; — rounds wrong under browser zoom and fractional DPR. &lt;code&gt;device-pixel-content-box&lt;/code&gt; hands you the exact integer the compositor uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SharpCanvas&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;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLCanvasElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useElementSize&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="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;box&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;device-pixel-content-box&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;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;width&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;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// physical pixels — pixel-perfect at any DPR or zoom&lt;/span&gt;
    &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;canvas&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;100%&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Safari doesn't support &lt;code&gt;device-pixel-content-box&lt;/code&gt; yet — the hook falls back to &lt;code&gt;contentRect&lt;/code&gt; there, so degrade gracefully rather than assuming physical pixels everywhere.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Real Patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Container-query components — breakpoints on the element, not the screen
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;ProductCard&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;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLDivElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useElementSize&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;layout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;640&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;horizontal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;320&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;compact&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stacked&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;article&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;data-layout&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;layout&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;layout&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;horizontal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SideBySide&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Stacked&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;article&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop this card in a sidebar, a modal, or a full-width list and it adapts to &lt;em&gt;its own&lt;/em&gt; space — no prop-drilling a &lt;code&gt;variant&lt;/code&gt; from whoever happens to know the context. Again: if the difference were only CSS, &lt;code&gt;@container&lt;/code&gt; does this cheaper. This pattern is for when the &lt;em&gt;component tree&lt;/em&gt; changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Responsive charts that re-layout at rest
&lt;/h3&gt;

&lt;p&gt;Chart libraries want pixel numbers, and re-computing a chart layout 60 times a second while the user drags a splitter is wasted work. Let CSS stretch the canvas visually and settle the real re-layout with &lt;a href="https://reactuse.com/state/usedebounce/" rel="noopener noreferrer"&gt;&lt;code&gt;useDebounce&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLDivElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;rawWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rawHeight&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useElementSize&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useDebounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawWidth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;150&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;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useDebounce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawHeight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// &amp;lt;ExpensiveChart width={width} height={height} /&amp;gt; re-lays-out&lt;/span&gt;
&lt;span class="c1"&gt;// once the drag stops, not on every frame of it.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How many columns fit?
&lt;/h3&gt;

&lt;p&gt;Grid math that CSS can't do — because the answer feeds &lt;code&gt;props&lt;/code&gt;, not styles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useElementSize&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;columns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&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="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;280&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;VirtualGrid&lt;/span&gt; &lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;columns&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  useElementSize vs Its Siblings
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@reactuses/core&lt;/code&gt; ships a small family of measurement hooks built on the same observer core — pick by what you need back:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hook&lt;/th&gt;
&lt;th&gt;Returns&lt;/th&gt;
&lt;th&gt;Reach for it when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://reactuse.com/element/useelementsize/" rel="noopener noreferrer"&gt;&lt;code&gt;useElementSize&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[width, height]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;you need dimensions, nothing else&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://reactuse.com/element/usemeasure/" rel="noopener noreferrer"&gt;&lt;code&gt;useMeasure&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;full &lt;code&gt;contentRect&lt;/code&gt; (&lt;code&gt;x/y/top/left/…&lt;/code&gt;) + a &lt;code&gt;stop()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;you want the whole rect, or to stop observing on demand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://reactuse.com/element/useelementbounding/" rel="noopener noreferrer"&gt;&lt;code&gt;useElementBounding&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;live &lt;code&gt;getBoundingClientRect&lt;/code&gt; — updates on scroll &lt;em&gt;and&lt;/em&gt; resize&lt;/td&gt;
&lt;td&gt;you need &lt;em&gt;where it is&lt;/em&gt; in the viewport, not just how big&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://reactuse.com/element/useresizeobserver/" rel="noopener noreferrer"&gt;&lt;code&gt;useResizeObserver&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;your callback gets raw entries&lt;/td&gt;
&lt;td&gt;side effects instead of state; imperative work per resize&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://reactuse.com/element/usewindowsize/" rel="noopener noreferrer"&gt;&lt;code&gt;useWindowSize&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;viewport &lt;code&gt;width/height&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;page-level layout, not element-level&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rule of thumb: &lt;code&gt;useElementSize&lt;/code&gt; for dimensions, &lt;a href="https://reactuse.com/element/useelementbounding/" rel="noopener noreferrer"&gt;&lt;code&gt;useElementBounding&lt;/code&gt;&lt;/a&gt; for position (tooltips, popovers, scroll-linked effects), &lt;a href="https://reactuse.com/element/useresizeobserver/" rel="noopener noreferrer"&gt;&lt;code&gt;useResizeObserver&lt;/code&gt;&lt;/a&gt; when you'd rather run code than store state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SSR is handled.&lt;/strong&gt; There's no DOM on the server, so the hook renders &lt;code&gt;[0, 0]&lt;/code&gt; and attaches the observer after hydration — no &lt;code&gt;typeof window&lt;/code&gt; guards in your code. Plan for the zero-frame: gate expensive children with &lt;code&gt;if (!width) return &amp;lt;Skeleton /&amp;gt;&lt;/code&gt; rather than letting a chart lay out at 0×0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The first real value arrives via the observer's initial report&lt;/strong&gt; — one extra render right after mount. That's the cost of correctness; don't fight it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Beware self-referential loops.&lt;/strong&gt; If the content you render &lt;em&gt;from&lt;/em&gt; the measured width changes the element's own width, you've built a resize feedback loop (&lt;code&gt;ResizeObserver loop completed with undelivered notifications&lt;/code&gt; in the console). Fix it by measuring a parent whose size you don't alter, or clamping the derived values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fragmented layouts are summed correctly.&lt;/strong&gt; In multi-column or paginated contexts an element's box can fragment; the hook sums &lt;code&gt;inlineSize&lt;/code&gt;/&lt;code&gt;blockSize&lt;/code&gt; across fragments per spec instead of reading only the first one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every hook call is one observer.&lt;/strong&gt; Measuring 500 virtualized rows individually means 500 observers — at that scale, drop down to a single &lt;a href="https://reactuse.com/element/useresizeobserver/" rel="noopener noreferrer"&gt;&lt;code&gt;useResizeObserver&lt;/code&gt;&lt;/a&gt; on the container, or observe one prototype row.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Element size ≠ window size. Sidebars, split panes, streaming content, and font loads all resize elements without touching the viewport — only a &lt;code&gt;ResizeObserver&lt;/code&gt; sees them, and &lt;a href="https://reactuse.com/element/useelementsize/" rel="noopener noreferrer"&gt;&lt;code&gt;useElementSize&lt;/code&gt;&lt;/a&gt; serves it as plain &lt;code&gt;[width, height]&lt;/code&gt; state.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;box&lt;/code&gt; option picks what you measure: &lt;code&gt;content-box&lt;/code&gt; for content math (default), &lt;code&gt;border-box&lt;/code&gt; for layout footprint, &lt;code&gt;device-pixel-content-box&lt;/code&gt; for pixel-perfect canvases at any DPR or zoom.&lt;/li&gt;
&lt;li&gt;Targets can be refs, elements, or lazy getters — use a getter for conditionally rendered elements; inline options don't churn the observer thanks to deep comparison — the bugs every hand-rolled version has, pre-fixed.&lt;/li&gt;
&lt;li&gt;Pure-CSS response to size? Use &lt;code&gt;@container&lt;/code&gt; queries. The hook is for when the number drives JavaScript: charts, canvas, virtualization, or swapping component trees.&lt;/li&gt;
&lt;li&gt;Need position too? That's &lt;a href="https://reactuse.com/element/useelementbounding/" rel="noopener noreferrer"&gt;&lt;code&gt;useElementBounding&lt;/code&gt;&lt;/a&gt;. The full rect plus manual stop? &lt;a href="https://reactuse.com/element/usemeasure/" rel="noopener noreferrer"&gt;&lt;code&gt;useMeasure&lt;/code&gt;&lt;/a&gt;. Raw entries? &lt;a href="https://reactuse.com/element/useresizeobserver/" rel="noopener noreferrer"&gt;&lt;code&gt;useResizeObserver&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;useElementSize&lt;/code&gt; and 110+ other SSR-safe, TypeScript-first hooks live in &lt;a href="https://reactuse.com" rel="noopener noreferrer"&gt;&lt;code&gt;@reactuses/core&lt;/code&gt;&lt;/a&gt; — one install, tree-shakeable, no dependencies to babysit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @reactuses/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>React useEventSource Hook: Server-Sent Events with Auto-Reconnect (2026)</title>
      <dc:creator>ReactUse</dc:creator>
      <pubDate>Wed, 12 Aug 2026 07:06:05 +0000</pubDate>
      <link>https://dev.to/childrentime/react-useeventsource-hook-server-sent-events-with-auto-reconnect-2026-fbm</link>
      <guid>https://dev.to/childrentime/react-useeventsource-hook-server-sent-events-with-auto-reconnect-2026-fbm</guid>
      <description>&lt;p&gt;Live notifications, deployment logs, stock tickers, AI responses streaming in token by token — none of these need a WebSocket. They're all one-directional: the server talks, the client listens. The browser has had a native protocol for exactly this since 2011 — &lt;strong&gt;Server-Sent Events (SSE)&lt;/strong&gt; — and it runs over plain HTTP, passes through proxies and load balancers untouched, and reconnects automatically when the connection drops.&lt;/p&gt;

&lt;p&gt;What SSE &lt;em&gt;doesn't&lt;/em&gt; have is a good React story. The native &lt;code&gt;EventSource&lt;/code&gt; API is imperative: you construct it, attach listeners, and must tear it down at exactly the right moment — the classic effect-lifecycle minefield. &lt;a href="https://reactuse.com/browser/useeventsource/" rel="noopener noreferrer"&gt;&lt;code&gt;useEventSource&lt;/code&gt;&lt;/a&gt; from &lt;a href="https://reactuse.com" rel="noopener noreferrer"&gt;&lt;code&gt;@reactuses/core&lt;/code&gt;&lt;/a&gt; turns the whole thing into declarative state: &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, and &lt;code&gt;error&lt;/code&gt; your component just renders. This post covers the hook's full API, the reconnect behavior that native &lt;code&gt;EventSource&lt;/code&gt; gets subtly wrong, and &lt;a href="https://reactuse.com/browser/usefetcheventsource/" rel="noopener noreferrer"&gt;&lt;code&gt;useFetchEventSource&lt;/code&gt;&lt;/a&gt; — the fetch-based variant you'll need the moment your stream requires an &lt;code&gt;Authorization&lt;/code&gt; header or a POST body, which in 2026 means every AI-completions endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @reactuses/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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;useEventSource&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@reactuses/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;DeploymentLog&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useEventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/deploy/stream&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CONNECTED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;🟢 live&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;🟡 connecting…&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;pre&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a complete live-updating component. The hook opens the connection on mount, updates &lt;code&gt;data&lt;/code&gt; on every message, exposes the connection lifecycle as &lt;code&gt;status&lt;/code&gt; (&lt;code&gt;"CONNECTING" | "CONNECTED" | "DISCONNECTED"&lt;/code&gt;), and closes the stream when the component unmounts. No refs, no listeners, no cleanup function to forget.&lt;/p&gt;

&lt;h2&gt;
  
  
  What SSE Actually Is (60 Seconds)
&lt;/h2&gt;

&lt;p&gt;Server-Sent Events is just an HTTP response that never finishes. The server replies with &lt;code&gt;Content-Type: text/event-stream&lt;/code&gt; and writes messages as plain text, separated by blank lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data: {"price": 101.42}
id: 7

event: trade
data: {"symbol": "ACME", "qty": 200}
id: 8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three field types matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;data:&lt;/code&gt; — the payload (always a string; JSON-encode structured data yourself).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;event:&lt;/code&gt; — an optional event &lt;em&gt;name&lt;/em&gt;, so one stream can carry multiple channels.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;id:&lt;/code&gt; — an optional event ID. The browser remembers the last one and sends it back as a &lt;code&gt;Last-Event-ID&lt;/code&gt; header when it reconnects, so a well-built server can resume where the client left off.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because it's plain HTTP, SSE works through corporate proxies, CDNs, and HTTP/2 multiplexing without the upgrade-handshake drama WebSockets sometimes hit. The trade-off: it's server → client only, and the native browser API can only send GET requests with no custom headers. Keep that limitation in mind — it's the reason the second hook in this post exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Manual Way — and Where It Bites
&lt;/h2&gt;

&lt;p&gt;Wiring &lt;code&gt;EventSource&lt;/code&gt; by hand looks manageable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ⚠️ hand-rolled — three bugs waiting to happen&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Ticker&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setPrice&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;es&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;EventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/prices&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;es&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;es&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problems show up in production, not in the demo:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reconnect is infinite and invisible.&lt;/strong&gt; When the server drops the connection, &lt;code&gt;EventSource&lt;/code&gt; retries forever, silently. If your API is down, every open tab hammers it every few seconds until the heat death of the universe — and you have no state telling the UI "we're offline" so you can show a banner or give up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Errors are opaque.&lt;/strong&gt; &lt;code&gt;onerror&lt;/code&gt; gives you a bare &lt;code&gt;Event&lt;/code&gt; — no status code, no reason. If you don't track connection state yourself, your UI happily shows stale data as if it were live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Named events need manual bookkeeping.&lt;/strong&gt; Every &lt;code&gt;event: trade&lt;/code&gt; line requires its own &lt;code&gt;addEventListener("trade", …)&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; a matching &lt;code&gt;removeEventListener&lt;/code&gt; in cleanup. Miss one and you leak listeners across React 18 StrictMode's mount-unmount-mount cycle.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of this is hard, exactly. It's just easy to get 90% right — which is the worst kind of wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The useEventSource API
&lt;/h2&gt;

&lt;p&gt;Everything the manual version does badly, as returned state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lastEventId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;close&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;eventSourceRef&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nf"&gt;useEventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;data: string | null&lt;/code&gt;&lt;/strong&gt; — payload of the most recent message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;event: string | null&lt;/code&gt;&lt;/strong&gt; — the name of the last &lt;em&gt;named&lt;/em&gt; event received (see below).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;status&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;"CONNECTING" | "CONNECTED" | "DISCONNECTED"&lt;/code&gt;. Render it; that's your live-indicator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;error: Event | null&lt;/code&gt;&lt;/strong&gt; — the last connection error, cleared on successful reconnect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;lastEventId: string | null&lt;/code&gt;&lt;/strong&gt; — the &lt;code&gt;id:&lt;/code&gt; field of the last message, i.e. your resume cursor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;open()&lt;/code&gt; / &lt;code&gt;close()&lt;/code&gt;&lt;/strong&gt; — manual control. &lt;code&gt;close()&lt;/code&gt; is &lt;em&gt;explicit&lt;/em&gt;: it also disables auto-reconnect, so "user clicked pause" stays paused. &lt;code&gt;open()&lt;/code&gt; reconnects and resets the retry counter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;eventSourceRef&lt;/code&gt;&lt;/strong&gt; — escape hatch to the raw &lt;code&gt;EventSource&lt;/code&gt; instance if you need it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Named events, declaratively
&lt;/h3&gt;

&lt;p&gt;Pass the event names you care about as the second argument, and the hook registers — and cleans up — every listener for you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useEventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/stream&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;trade&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;quote&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="c1"&gt;// event === "trade" | "quote" | null — which channel data came from&lt;/span&gt;
&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;trade&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;appendTrade&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;!&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="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Auto-reconnect with a budget
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;autoReconnect&lt;/code&gt; option replaces &lt;code&gt;EventSource&lt;/code&gt;'s silent infinite retry with a policy you choose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useEventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/notifications&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="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;autoReconnect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// give up after 5 attempts (or pass a () =&amp;gt; boolean)&lt;/span&gt;
    &lt;span class="na"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// wait 2s between attempts&lt;/span&gt;
    &lt;span class="na"&gt;onFailed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;toast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Live updates unavailable — refresh to retry&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;retries&lt;/code&gt; defaults to &lt;code&gt;-1&lt;/code&gt; (retry forever, matching native behavior), but now it's a &lt;em&gt;decision&lt;/em&gt; rather than a surprise, and &lt;code&gt;onFailed&lt;/code&gt; gives you the moment to tell the user. Pair it with &lt;code&gt;status === "DISCONNECTED"&lt;/code&gt; to render a degraded-mode UI instead of silently stale numbers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connect lazily
&lt;/h3&gt;

&lt;p&gt;By default the hook connects on mount. Pass &lt;code&gt;immediate: false&lt;/code&gt; to wait for user intent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;close&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useEventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/live-scores&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="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;immediate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CONNECTED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;close&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CONNECTED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Pause live scores&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Go live&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Wall Every SSE Tutorial Hits: Auth Headers
&lt;/h2&gt;

&lt;p&gt;Here is the native API's dirty secret: &lt;code&gt;new EventSource(url)&lt;/code&gt; &lt;strong&gt;cannot send custom headers&lt;/strong&gt;. No &lt;code&gt;Authorization: Bearer …&lt;/code&gt;, no &lt;code&gt;X-Api-Key&lt;/code&gt;, nothing. Your options with the native API are cookies (&lt;code&gt;withCredentials: true&lt;/code&gt;) or a token in the query string — one of which doesn't work cross-domain with modern cookie policies, and the other of which lands your token in every access log between the browser and your server.&lt;/p&gt;

&lt;p&gt;It also can't POST. That matters because the biggest SSE consumers of 2026 — OpenAI-style AI completion endpoints — are all &lt;code&gt;POST /v1/chat/completions&lt;/code&gt; with a JSON body and a bearer token, streaming back &lt;code&gt;text/event-stream&lt;/code&gt;. The native &lt;code&gt;EventSource&lt;/code&gt; API literally cannot call them.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://reactuse.com/browser/usefetcheventsource/" rel="noopener noreferrer"&gt;&lt;code&gt;useFetchEventSource&lt;/code&gt;&lt;/a&gt; solves this by speaking SSE over &lt;code&gt;fetch&lt;/code&gt; (built on Microsoft's battle-tested &lt;a href="https://github.com/Azure/fetch-event-source" rel="noopener noreferrer"&gt;&lt;code&gt;fetch-event-source&lt;/code&gt;&lt;/a&gt; parser), which means the full request is yours to shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useFetchEventSource&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@reactuses/core&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useFetchEventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/v1/chat/completions&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-5&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;autoReconnect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&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;Same return shape as &lt;code&gt;useEventSource&lt;/code&gt; — &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;event&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;error&lt;/code&gt;, &lt;code&gt;lastEventId&lt;/code&gt;, &lt;code&gt;open&lt;/code&gt;, &lt;code&gt;close&lt;/code&gt; — so switching between the two is a one-line change, not a rewrite.&lt;/p&gt;

&lt;h3&gt;
  
  
  Streaming an AI response, token by token
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;onMessage&lt;/code&gt; callback is the natural place to accumulate a streamed completion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Answer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setText&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useFetchEventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/ask&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;onMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;msg&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;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[DONE]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;choices&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="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nf"&gt;setText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;onError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&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="nf"&gt;isRateLimit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// return a number = retry after N ms&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Markdown&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CONNECTED&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;▌&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Markdown&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details worth stealing: returning a number from &lt;code&gt;onError&lt;/code&gt; overrides the reconnect delay for that attempt (perfect for &lt;code&gt;Retry-After&lt;/code&gt;-style backoff), and the functional &lt;code&gt;setText(prev =&amp;gt; …)&lt;/code&gt; update means token order survives React's batching.&lt;/p&gt;

&lt;h3&gt;
  
  
  Native or fetch-based — which one?
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;a href="https://reactuse.com/browser/useeventsource/" rel="noopener noreferrer"&gt;&lt;code&gt;useEventSource&lt;/code&gt;&lt;/a&gt;&lt;/th&gt;
&lt;th&gt;&lt;a href="https://reactuse.com/browser/usefetcheventsource/" rel="noopener noreferrer"&gt;&lt;code&gt;useFetchEventSource&lt;/code&gt;&lt;/a&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Transport&lt;/td&gt;
&lt;td&gt;native &lt;code&gt;EventSource&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;fetch&lt;/code&gt; + stream parser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Custom headers / bearer auth&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST with body&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto &lt;code&gt;Last-Event-ID&lt;/code&gt; resume&lt;/td&gt;
&lt;td&gt;✅ built-in&lt;/td&gt;
&lt;td&gt;your server's job&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extra bundle weight&lt;/td&gt;
&lt;td&gt;zero&lt;/td&gt;
&lt;td&gt;small parser dependency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reach for it when…&lt;/td&gt;
&lt;td&gt;same-origin or cookie-auth streams&lt;/td&gt;
&lt;td&gt;AI APIs, token auth, request bodies&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Simple rule: start with &lt;code&gt;useEventSource&lt;/code&gt;; the moment you type the word &lt;code&gt;Authorization&lt;/code&gt;, switch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Production Notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SSR is handled.&lt;/strong&gt; Both hooks touch &lt;code&gt;EventSource&lt;/code&gt;/&lt;code&gt;fetch&lt;/code&gt; only inside effects, so they render harmlessly on the server — no &lt;code&gt;typeof window&lt;/code&gt; guards in your code. First paint shows &lt;code&gt;status: "DISCONNECTED"&lt;/code&gt;, then the client connects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pause hidden tabs.&lt;/strong&gt; A dashboard left open in a background tab keeps its stream (and your server's connection budget) alive. Combine with &lt;a href="https://reactuse.com/element/usedocumentvisibility/" rel="noopener noreferrer"&gt;&lt;code&gt;useDocumentVisibility&lt;/code&gt;&lt;/a&gt; to &lt;code&gt;close()&lt;/code&gt; when hidden and &lt;code&gt;open()&lt;/code&gt; on return — the &lt;code&gt;Last-Event-ID&lt;/code&gt; handshake makes resume cheap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One tab streams, the rest listen.&lt;/strong&gt; Browsers cap concurrent connections per origin over HTTP/1.1 (~6), and every open tab with an SSE stream burns one. The classic fix: hold the stream in one tab and fan messages out with &lt;a href="https://reactuse.com/browser/usebroadcastchannel/" rel="noopener noreferrer"&gt;&lt;code&gt;useBroadcastChannel&lt;/code&gt;&lt;/a&gt;. (Or serve over HTTP/2, where streams multiplex.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't reconnect into a dead network.&lt;/strong&gt; &lt;a href="https://reactuse.com/browser/usenetwork/" rel="noopener noreferrer"&gt;&lt;code&gt;useNetwork&lt;/code&gt;&lt;/a&gt; or the smaller &lt;a href="https://reactuse.com/browser/useonline/" rel="noopener noreferrer"&gt;&lt;code&gt;useOnline&lt;/code&gt;&lt;/a&gt; tells you the browser is offline — gate your retry UI on it instead of burning the retry budget while the laptop is in a tunnel.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When SSE Is the Wrong Tool
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The client needs to talk back on the same channel&lt;/strong&gt; — chat where you &lt;em&gt;send&lt;/em&gt; messages, multiplayer cursors, collaborative editing. That's bidirectional; use a WebSocket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Updates are rare.&lt;/strong&gt; A value that changes a few times an hour doesn't justify a held-open connection — poll it, or refetch on focus with your data library.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're delivering one payload.&lt;/strong&gt; If the response ends when the data arrives, that's just &lt;code&gt;fetch&lt;/code&gt;. SSE earns its keep only when the stream outlives the request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Binary data.&lt;/strong&gt; SSE is UTF-8 text. Ship binary over WebSocket or chunked &lt;code&gt;fetch&lt;/code&gt; instead of base64-ing it through a text stream.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;SSE is the simplest real-time transport: one long-lived HTTP response, native browser support, automatic resume via &lt;code&gt;Last-Event-ID&lt;/code&gt; — right for every server-to-client feed.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://reactuse.com/browser/useeventsource/" rel="noopener noreferrer"&gt;&lt;code&gt;useEventSource&lt;/code&gt;&lt;/a&gt; turns the imperative &lt;code&gt;EventSource&lt;/code&gt; lifecycle into rendered state (&lt;code&gt;data&lt;/code&gt; / &lt;code&gt;status&lt;/code&gt; / &lt;code&gt;error&lt;/code&gt;), handles named-event listener cleanup, and replaces invisible infinite retry with a reconnect policy you set — &lt;code&gt;retries&lt;/code&gt;, &lt;code&gt;delay&lt;/code&gt;, &lt;code&gt;onFailed&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Native &lt;code&gt;EventSource&lt;/code&gt; can't send an &lt;code&gt;Authorization&lt;/code&gt; header or a POST body. &lt;a href="https://reactuse.com/browser/usefetcheventsource/" rel="noopener noreferrer"&gt;&lt;code&gt;useFetchEventSource&lt;/code&gt;&lt;/a&gt; can — same API shape, fetch-based transport — and it's the piece you need for streaming AI completions.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;close()&lt;/code&gt; means &lt;em&gt;stay closed&lt;/em&gt; (no auto-reconnect); &lt;code&gt;open()&lt;/code&gt; resets the retry budget. Wire them to visibility and network state for streams that behave like a good citizen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;useEventSource&lt;/code&gt;, &lt;code&gt;useFetchEventSource&lt;/code&gt;, and 110+ other SSR-safe, TypeScript-first hooks live in &lt;a href="https://reactuse.com" rel="noopener noreferrer"&gt;&lt;code&gt;@reactuses/core&lt;/code&gt;&lt;/a&gt; — one install, tree-shakeable, no dependencies to babysit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @reactuses/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>React useMount Hook: Run an Effect Once on Mount (2026)</title>
      <dc:creator>ReactUse</dc:creator>
      <pubDate>Tue, 11 Aug 2026 02:51:06 +0000</pubDate>
      <link>https://dev.to/childrentime/react-usemount-hook-run-an-effect-once-on-mount-2026-15g5</link>
      <guid>https://dev.to/childrentime/react-usemount-hook-run-an-effect-once-on-mount-2026-15g5</guid>
      <description>&lt;p&gt;"Run this once, when the component appears." It's the single most common effect in React — focus an input, fire an analytics event, open a connection, read from a browser API. The idiom everyone reaches for is &lt;code&gt;useEffect&lt;/code&gt; with an empty dependency array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;trackPageView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/checkout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// eslint-disable-next-line react-hooks/exhaustive-deps&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, but look at what it costs: an empty array you must remember (forget it and the effect runs on &lt;em&gt;every&lt;/em&gt; render), a lint suppression comment whenever the effect touches anything, and — worst of all — zero stated intent. &lt;code&gt;useEffect(fn, [])&lt;/code&gt; says &lt;em&gt;how&lt;/em&gt;; it never says &lt;em&gt;why&lt;/em&gt;. Six months later, a teammate adds a dependency to that array "to fix the lint warning" and your run-once effect quietly becomes a run-on-change effect.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://reactuse.com/effect/usemount/" rel="noopener noreferrer"&gt;&lt;code&gt;useMount&lt;/code&gt;&lt;/a&gt; from &lt;a href="https://reactuse.com" rel="noopener noreferrer"&gt;&lt;code&gt;@reactuses/core&lt;/code&gt;&lt;/a&gt; is the named version of this idiom: &lt;code&gt;useMount(fn)&lt;/code&gt; runs &lt;code&gt;fn&lt;/code&gt; exactly once per mount, and the name &lt;em&gt;is&lt;/em&gt; the documentation. This post covers what it actually compiles down to, the React 18+ StrictMode double-run that surprises everyone the first time, the subtle stale-closure bug in hand-rolled unmount cleanups (and how &lt;a href="https://reactuse.com/effect/useunmount/" rel="noopener noreferrer"&gt;&lt;code&gt;useUnmount&lt;/code&gt;&lt;/a&gt; dodges it), async work on mount, and — just as important — the cases where you should &lt;em&gt;not&lt;/em&gt; use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @reactuses/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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;useMount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useUnmount&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@reactuses/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;SearchBox&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;inputRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useMount&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;inputRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;useUnmount&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;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="s2"&gt;search box removed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;inputRef&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Search…"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No dependency array, no lint comment, and the reader knows the intent before reading the body: this runs on mount, full stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  What useMount Actually Is
&lt;/h2&gt;

&lt;p&gt;No magic — here is the entire implementation, minus a dev-only type check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useMount&lt;/span&gt; &lt;span class="o"&gt;=&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;?.();&lt;/span&gt;
    &lt;span class="c1"&gt;// eslint-disable-next-line react-hooks/exhaustive-deps&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;That's it: &lt;code&gt;useEffect&lt;/code&gt; with an empty array, wrapped once so &lt;em&gt;you&lt;/em&gt; never write the array or the suppression comment again. Three things fall out of this five-line definition:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Timing is &lt;code&gt;useEffect&lt;/code&gt; timing.&lt;/strong&gt; The callback fires after the component is committed to the DOM — after first paint, browser APIs available. It is not &lt;code&gt;useLayoutEffect&lt;/code&gt;; if you need to measure and mutate before paint, reach for a layout effect instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is SSR-safe by construction.&lt;/strong&gt; Effects simply never run on the server, so &lt;code&gt;useMount&lt;/code&gt; is the natural home for &lt;code&gt;window&lt;/code&gt;/&lt;code&gt;document&lt;/code&gt; access in SSR apps — the same guarantee the manual &lt;code&gt;useEffect(fn, [])&lt;/code&gt; gives you, with the intent spelled out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The return value is ignored.&lt;/strong&gt; &lt;code&gt;useMount&lt;/code&gt; calls &lt;code&gt;fn?.()&lt;/code&gt; and discards the result — it does &lt;strong&gt;not&lt;/strong&gt; forward a returned function to React as cleanup. Cleanup belongs to &lt;code&gt;useUnmount&lt;/code&gt; (below). A side effect of this design: passing an &lt;code&gt;async&lt;/code&gt; function is safe, which is more than you can say for raw &lt;code&gt;useEffect&lt;/code&gt; (more on that in a minute).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One consequence to internalize: because the dependency array is empty, the callback closes over &lt;strong&gt;first-render values&lt;/strong&gt;. Props and state read inside &lt;code&gt;useMount&lt;/code&gt; are frozen at their initial values. For a mount effect that's almost always what you want — but if you find yourself wanting fresh values inside, that's the signal you actually want &lt;code&gt;useEffect&lt;/code&gt; with dependencies, or a &lt;a href="https://reactuse.com/state/uselatest/" rel="noopener noreferrer"&gt;&lt;code&gt;useLatest&lt;/code&gt;&lt;/a&gt; ref.&lt;/p&gt;

&lt;h2&gt;
  
  
  The StrictMode Gotcha: "Why Does My Mount Effect Run Twice?"
&lt;/h2&gt;

&lt;p&gt;Search "useEffect runs twice" and you'll find a decade of confusion. Here's the short version: since React 18, &lt;code&gt;&amp;lt;StrictMode&amp;gt;&lt;/code&gt; in &lt;strong&gt;development&lt;/strong&gt; deliberately mounts every component, unmounts it, and mounts it again. Any mount effect — &lt;code&gt;useEffect(fn, [])&lt;/code&gt;, &lt;code&gt;useMount&lt;/code&gt;, doesn't matter — runs twice in dev. In production it runs once.&lt;/p&gt;

&lt;p&gt;React does this on purpose, to surface effects that don't clean up after themselves. The official guidance is: don't fight the double-run, make the effect &lt;strong&gt;idempotent&lt;/strong&gt; — running it twice should be harmless because the cleanup undoes the first run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useMount&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="nx"&gt;controller&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;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/config&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="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;applyConfig&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// pair with useUnmount(() =&amp;gt; controller.abort())&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But some effects are &lt;em&gt;genuinely&lt;/em&gt; once-only, and running them twice is a real bug, not a hygiene warning: an analytics beacon fires twice, a welcome toast pops twice, a payment-intent gets created twice in dev and QA files a ticket. For those, &lt;code&gt;@reactuses/core&lt;/code&gt; ships &lt;a href="https://reactuse.com/effect/useonceeffect/" rel="noopener noreferrer"&gt;&lt;code&gt;useOnceEffect&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useOnceEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@reactuses/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;useOnceEffect&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;trackPageView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/checkout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// fires once, even under StrictMode&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trick inside is elegant: &lt;code&gt;useOnceEffect&lt;/code&gt; records each effect function in a &lt;code&gt;WeakSet&lt;/code&gt; before running it. StrictMode's remount re-invokes the &lt;em&gt;same&lt;/em&gt; effect function instance from the same render, so the second invocation finds it already recorded and bails. Genuine remounts (the component actually left and came back) create a fresh function and run again — exactly the semantics "once per mount, ignoring StrictMode's rehearsal" implies.&lt;/p&gt;

&lt;p&gt;Rule of thumb: &lt;strong&gt;&lt;code&gt;useMount&lt;/code&gt; + idempotent by default; &lt;code&gt;useOnceEffect&lt;/code&gt; when a double-fire is observable to a user or a backend.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  useUnmount — and the Stale-Closure Trap It Avoids
&lt;/h2&gt;

&lt;p&gt;The obvious hand-rolled unmount cleanup has a bug most people ship without noticing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ⚠️ hand-rolled version&lt;/span&gt;
&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;saveDraft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// draft from the FIRST render — always empty!&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;// empty deps ⇒ the cleanup closure was created on render #1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cleanup function was created on the first render, so it captured the first render's &lt;code&gt;draft&lt;/code&gt;. When the component unmounts three minutes and forty keystrokes later, it saves an empty string. The "fix" of adding &lt;code&gt;draft&lt;/code&gt; to the deps is worse — now the cleanup runs on every keystroke, not on unmount.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://reactuse.com/effect/useunmount/" rel="noopener noreferrer"&gt;&lt;code&gt;useUnmount&lt;/code&gt;&lt;/a&gt; solves this properly. Internally it stores your callback in a &lt;a href="https://reactuse.com/state/uselatest/" rel="noopener noreferrer"&gt;&lt;code&gt;useLatest&lt;/code&gt;&lt;/a&gt; ref that's updated every render, and the unmount cleanup calls through the ref:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useUnmount&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@reactuses/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Composer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setDraft&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useUnmount&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;saveDraft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ✅ the draft as of the LAST render&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;textarea&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setDraft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&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="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your callback runs exactly once, at unmount, and sees the latest state. This is the concrete reason to prefer &lt;code&gt;useUnmount&lt;/code&gt; over the &lt;code&gt;return () =&amp;gt; {}&lt;/code&gt; idiom whenever the cleanup reads state or props — it's not sugar, it's a bug fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Async Work on Mount
&lt;/h2&gt;

&lt;p&gt;Raw &lt;code&gt;useEffect&lt;/code&gt; famously rejects async functions — &lt;code&gt;useEffect(async () =&amp;gt; {...}, [])&lt;/code&gt; hands React a Promise where it expects a cleanup function, and you get a warning plus a skipped cleanup. Because &lt;code&gt;useMount&lt;/code&gt; discards the callback's return value, an async callback is perfectly fine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="nf"&gt;useMount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &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="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchCurrentUser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&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;One thing this doesn't give you is protection against the component unmounting mid-&lt;code&gt;await&lt;/code&gt; — calling &lt;code&gt;setUser&lt;/code&gt; after unmount is harmless in React 18+ but often still not what you want (you may be writing to state that a remounted instance will clobber). Two library answers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://reactuse.com/state/usemountedstate/" rel="noopener noreferrer"&gt;&lt;code&gt;useMountedState&lt;/code&gt;&lt;/a&gt; returns an &lt;code&gt;isMounted()&lt;/code&gt; function backed by a ref — check it after each &lt;code&gt;await&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isMounted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMountedState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;useMount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &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="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchCurrentUser&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="nf"&gt;isMounted&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="nf"&gt;setUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&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;ul&gt;
&lt;li&gt;
&lt;a href="https://reactuse.com/effect/useasynceffect/" rel="noopener noreferrer"&gt;&lt;code&gt;useAsyncEffect&lt;/code&gt;&lt;/a&gt; generalizes the pattern for effects with dependencies, handing your async body a liveness check and supporting cleanup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For real data fetching with caching and retries you'll outgrow both — that's React Query / SWR territory, or your framework's loaders. &lt;code&gt;useMount&lt;/code&gt; is for the one-shot imperative stuff around the edges.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mirror Image: Skipping the Mount
&lt;/h2&gt;

&lt;p&gt;Sometimes you want the opposite — react to &lt;em&gt;changes&lt;/em&gt; but not to the initial mount. Sync a filter to the URL, but don't rewrite the URL on first load; show "settings saved" on change, but not on arrival. That's &lt;a href="https://reactuse.com/effect/useupdateeffect/" rel="noopener noreferrer"&gt;&lt;code&gt;useUpdateEffect&lt;/code&gt;&lt;/a&gt;, and its primitive sibling &lt;a href="https://reactuse.com/state/usefirstmountstate/" rel="noopener noreferrer"&gt;&lt;code&gt;useFirstMountState&lt;/code&gt;&lt;/a&gt; which simply tells you whether this is the first render:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useUpdateEffect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@reactuses/core&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;useUpdateEffect&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;syncFilterToUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// runs on filter changes, skips the mount&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Together the four hooks cover the whole lifecycle vocabulary that classes used to spell &lt;code&gt;componentDidMount&lt;/code&gt; / &lt;code&gt;componentDidUpdate&lt;/code&gt; / &lt;code&gt;componentWillUnmount&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;You want to run code…&lt;/th&gt;
&lt;th&gt;Reach for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;once, after the component appears&lt;/td&gt;
&lt;td&gt;&lt;a href="https://reactuse.com/effect/usemount/" rel="noopener noreferrer"&gt;&lt;code&gt;useMount&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;once, even under StrictMode's dev double-run&lt;/td&gt;
&lt;td&gt;&lt;a href="https://reactuse.com/effect/useonceeffect/" rel="noopener noreferrer"&gt;&lt;code&gt;useOnceEffect&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;when the component is removed, seeing latest state&lt;/td&gt;
&lt;td&gt;&lt;a href="https://reactuse.com/effect/useunmount/" rel="noopener noreferrer"&gt;&lt;code&gt;useUnmount&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;on updates only, skipping the first render&lt;/td&gt;
&lt;td&gt;&lt;a href="https://reactuse.com/effect/useupdateeffect/" rel="noopener noreferrer"&gt;&lt;code&gt;useUpdateEffect&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;conditionally, based on "is this the first render?"&lt;/td&gt;
&lt;td&gt;&lt;a href="https://reactuse.com/state/usefirstmountstate/" rel="noopener noreferrer"&gt;&lt;code&gt;useFirstMountState&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  When &lt;em&gt;Not&lt;/em&gt; to Use useMount
&lt;/h2&gt;

&lt;p&gt;Honesty section. &lt;code&gt;useMount&lt;/code&gt; is intent-naming sugar over a real React primitive, and the primitive is sometimes the right call:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The effect reads a prop or state that can change.&lt;/strong&gt; If &lt;code&gt;roomId&lt;/code&gt; changes and you need to reconnect, that is &lt;code&gt;useEffect(connect, [roomId])&lt;/code&gt; — a mount hook here is a synchronization bug wearing a convenience API. The empty array isn't ceremony in that case; it's wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You're fetching server data for rendering.&lt;/strong&gt; Framework loaders, React Query, SWR — anything with caching, deduplication, and revalidation beats a fetch in a mount effect. React's own docs have walked away from "fetch in useEffect" as a primary pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need pre-paint measurement.&lt;/strong&gt; &lt;code&gt;useMount&lt;/code&gt; is post-paint. Measure-then-mutate work belongs in a layout effect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The "mount event" is actually a user event.&lt;/strong&gt; If code can run in the click handler that caused the component to appear, run it there — effects are for synchronizing with external systems, not a junk drawer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The test is one question: &lt;em&gt;would this code ever need to re-run while the component is alive?&lt;/em&gt; If the answer is any form of "yes, when X changes," you want &lt;code&gt;useEffect&lt;/code&gt; and a dependency array. If it's a clean "no," &lt;code&gt;useMount&lt;/code&gt; says so in a way &lt;code&gt;[]&lt;/code&gt; never will.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;useMount(fn)&lt;/code&gt; is &lt;code&gt;useEffect(fn, [])&lt;/code&gt; with the intent in the name — no array to forget, no lint suppression, first-render closure semantics you should embrace, not fight.&lt;/li&gt;
&lt;li&gt;In React 18+ dev StrictMode every mount effect runs twice. Make effects idempotent by default; use &lt;a href="https://reactuse.com/effect/useonceeffect/" rel="noopener noreferrer"&gt;&lt;code&gt;useOnceEffect&lt;/code&gt;&lt;/a&gt; when a double-fire is user- or backend-visible.&lt;/li&gt;
&lt;li&gt;Hand-rolled &lt;code&gt;return () =&amp;gt; {}&lt;/code&gt; cleanups with empty deps capture first-render state — a real, shipping bug. &lt;a href="https://reactuse.com/effect/useunmount/" rel="noopener noreferrer"&gt;&lt;code&gt;useUnmount&lt;/code&gt;&lt;/a&gt; reads through a latest-ref and sees final state.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;useMount&lt;/code&gt; accepts &lt;code&gt;async&lt;/code&gt; callbacks (the return value is discarded); guard post-&lt;code&gt;await&lt;/code&gt; state writes with &lt;a href="https://reactuse.com/state/usemountedstate/" rel="noopener noreferrer"&gt;&lt;code&gt;useMountedState&lt;/code&gt;&lt;/a&gt; or use &lt;a href="https://reactuse.com/effect/useasynceffect/" rel="noopener noreferrer"&gt;&lt;code&gt;useAsyncEffect&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;For change-only effects, &lt;a href="https://reactuse.com/effect/useupdateeffect/" rel="noopener noreferrer"&gt;&lt;code&gt;useUpdateEffect&lt;/code&gt;&lt;/a&gt; is the mirror image.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;useMount&lt;/code&gt;, &lt;code&gt;useUnmount&lt;/code&gt;, &lt;code&gt;useOnceEffect&lt;/code&gt;, and 110+ other SSR-safe, TypeScript-first hooks live in &lt;a href="https://reactuse.com" rel="noopener noreferrer"&gt;&lt;code&gt;@reactuses/core&lt;/code&gt;&lt;/a&gt; — one install, tree-shakeable, no dependencies to babysit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @reactuses/core
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>React useEvent Hook: Stable Callbacks Without Stale Closures (2026)</title>
      <dc:creator>ReactUse</dc:creator>
      <pubDate>Fri, 07 Aug 2026 03:59:18 +0000</pubDate>
      <link>https://dev.to/childrentime/react-useevent-hook-stable-callbacks-without-stale-closures-2026-131b</link>
      <guid>https://dev.to/childrentime/react-useevent-hook-stable-callbacks-without-stale-closures-2026-131b</guid>
      <description>&lt;p&gt;Every React developer eventually meets the same fork in the road. You write an event handler that reads state, pass it to a child or an effect, and now you must choose: leave it as a plain inline function and watch every render create a new reference — breaking &lt;code&gt;React.memo&lt;/code&gt;, re-running effects, re-subscribing listeners — or wrap it in &lt;code&gt;useCallback&lt;/code&gt; and start playing dependency-array whack-a-mole, where one forgotten dependency means the handler sees state from three renders ago.&lt;/p&gt;

&lt;p&gt;That second failure mode has a name — the &lt;strong&gt;stale closure&lt;/strong&gt; — and it's arguably the most common React bug in production code. The fix has a name too: &lt;code&gt;useEvent&lt;/code&gt;, proposed in an &lt;a href="https://github.com/reactjs/rfcs/blob/main/text/0000-useevent.md" rel="noopener noreferrer"&gt;official React RFC in 2022&lt;/a&gt;, and available today as &lt;code&gt;useEvent&lt;/code&gt; in &lt;code&gt;@reactuses/core&lt;/code&gt;. It gives you a function whose &lt;strong&gt;identity never changes across renders&lt;/strong&gt; but whose body &lt;strong&gt;always sees the latest state and props&lt;/strong&gt;. Both halves of the fork, no trade-off.&lt;/p&gt;

&lt;p&gt;This post covers the API, the three-line implementation trick that makes it work, how it compares to &lt;code&gt;useCallback&lt;/code&gt; and to React 19.2's built-in &lt;code&gt;useEffectEvent&lt;/code&gt;, real patterns, and the one rule you must respect (don't call it during render). TypeScript-first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem in Thirty Seconds
&lt;/h2&gt;

&lt;p&gt;Here's the bug factory. A chat component sends a heartbeat with the current draft text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Composer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;roomId&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setDraft&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&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="nf"&gt;sendHeartbeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ⚠️ which draft?&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&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="nx"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// draft intentionally omitted — we don't want to reset the timer&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;textarea&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setDraft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&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="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interval closes over the &lt;code&gt;draft&lt;/code&gt; that existed when the effect ran — the empty string. Every heartbeat sends &lt;code&gt;''&lt;/code&gt; forever. Add &lt;code&gt;draft&lt;/code&gt; to the dependency array and the closure is fresh, but now the interval tears down and restarts on &lt;strong&gt;every keystroke&lt;/strong&gt;. &lt;code&gt;useCallback&lt;/code&gt; doesn't help: it has the exact same dependency array, so it forces the exact same choice — stale values or churning identity.&lt;/p&gt;

&lt;p&gt;What you actually want is a function that is &lt;em&gt;one stable thing&lt;/em&gt; over the component's lifetime, but &lt;em&gt;reads current values&lt;/em&gt; whenever it fires. That's &lt;code&gt;useEvent&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEvent&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;@reactuses/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Composer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;roomId&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setDraft&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="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;beat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useEvent&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;sendHeartbeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ✅ always the latest draft and roomId&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&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="nx"&gt;beat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;clearInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&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="nx"&gt;beat&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// beat never changes — effect runs once&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;textarea&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;draft&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onChange&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setDraft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&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="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;beat&lt;/code&gt; is referentially identical on every render, so the effect runs once and the interval survives typing. When it fires, it reads &lt;code&gt;draft&lt;/code&gt; through the latest render's closure. The dependency array is even honest — &lt;code&gt;beat&lt;/code&gt; is listed, it just happens to be stable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Full API
&lt;/h2&gt;

&lt;p&gt;There's almost nothing to learn:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stableFn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useEvent&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;fn&lt;/code&gt;&lt;/strong&gt; — any function. Arguments and return value pass straight through, &lt;code&gt;this&lt;/code&gt; included.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;stableFn&lt;/code&gt;&lt;/strong&gt; — same TypeScript type as &lt;code&gt;fn&lt;/code&gt;, but its identity is fixed for the lifetime of the component.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The typing is exact, not &lt;code&gt;(...args: any[]) =&amp;gt; any&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;format&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useEvent&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// ✅ string&lt;/span&gt;
&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ❌ type error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In development, passing a non-function logs &lt;code&gt;useEvent expected parameter is a function, got …&lt;/code&gt; to the console instead of failing silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works Inside
&lt;/h2&gt;

&lt;p&gt;The entire implementation is short enough to read over coffee, and every line earns its place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useEvent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Fn&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;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="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="nx"&gt;handlerRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&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="nf"&gt;useIsomorphicLayoutEffect&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;handlerRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&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="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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;((...&lt;/span&gt;&lt;span class="nx"&gt;args&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="nx"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;handlerRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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;fn&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details worth noticing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A ref carries the latest closure.&lt;/strong&gt; Each render produces a fresh &lt;code&gt;fn&lt;/code&gt; closing over fresh state; the effect stashes it in &lt;code&gt;handlerRef&lt;/code&gt;. The returned wrapper — memoized once with an empty dependency array — reads &lt;code&gt;handlerRef.current&lt;/code&gt; &lt;em&gt;at call time&lt;/em&gt;, not at render time. Stable shell, fresh core.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The ref updates in a layout effect, not a passive effect.&lt;/strong&gt; &lt;code&gt;useIsomorphicLayoutEffect&lt;/code&gt; runs synchronously after DOM mutation, &lt;em&gt;before&lt;/em&gt; the browser paints and before passive &lt;code&gt;useEffect&lt;/code&gt; callbacks. If the ref were updated in a plain &lt;code&gt;useEffect&lt;/code&gt;, any event that fired in the gap — or any other effect running earlier in the same commit — could call the wrapper and hit the previous render's closure. The layout timing closes that window.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Isomorphic means SSR-safe.&lt;/strong&gt; &lt;code&gt;useLayoutEffect&lt;/code&gt; on the server prints a hydration warning; &lt;code&gt;useIsomorphicLayoutEffect&lt;/code&gt; swaps in &lt;code&gt;useEffect&lt;/code&gt; during SSR and the real thing in the browser. No warnings, no special-casing in your code.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If this ref-holding trick sounds familiar, it's the same idea as &lt;code&gt;useLatest&lt;/code&gt; — &lt;code&gt;useEvent&lt;/code&gt; is essentially &lt;code&gt;useLatest&lt;/code&gt; plus a stable callable wrapper. Reach for &lt;code&gt;useLatest&lt;/code&gt; when you want to &lt;em&gt;read&lt;/em&gt; a fresh value inside some existing callback; reach for &lt;code&gt;useEvent&lt;/code&gt; when the callback itself is the thing you're passing around.&lt;/p&gt;

&lt;h2&gt;
  
  
  useEvent vs useCallback
&lt;/h2&gt;

&lt;p&gt;They solve different problems, and the comparison makes both clearer:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;useCallback&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;useEvent&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Identity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Changes whenever deps change&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Never changes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Closure freshness&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Only as fresh as your dep array is correct&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Always latest&lt;/strong&gt; — read at call time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dependency array&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Required; the bug surface&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Callable during render?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;❌ No — event/effect time only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Values computed &lt;em&gt;during&lt;/em&gt; render (memoized selectors, render props)&lt;/td&gt;
&lt;td&gt;Handlers &lt;em&gt;fired&lt;/em&gt; later (events, timers, subscriptions)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The render-time row is the real dividing line. &lt;code&gt;useCallback&lt;/code&gt;'s result is an ordinary value — you can call it while rendering to compute JSX. &lt;code&gt;useEvent&lt;/code&gt;'s wrapper reads a ref that is only guaranteed current &lt;em&gt;after&lt;/em&gt; commit, so calling it during render can observe a previous render's state (and breaks the concurrent-rendering contract the RFC was careful about). The rule of thumb writes itself: &lt;strong&gt;if the function fires in response to something — a click, a tick, a message — use &lt;code&gt;useEvent&lt;/code&gt;. If it computes something during render, use &lt;code&gt;useCallback&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  useEvent vs React's useEffectEvent
&lt;/h2&gt;

&lt;p&gt;The 2022 RFC was ultimately superseded: React shipped the idea as &lt;a href="https://react.dev/reference/react/useEffectEvent" rel="noopener noreferrer"&gt;&lt;code&gt;useEffectEvent&lt;/code&gt;&lt;/a&gt;, stable since React 19.2. If you're on 19.2+ you should know how the two relate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;useEffectEvent&lt;/code&gt; is deliberately narrower.&lt;/strong&gt; The returned function may only be called from &lt;em&gt;inside effects&lt;/em&gt; (the ESLint rule enforces it), and must not be passed to other components or hooks. React's team scoped it to the one pattern they considered airtight: reading fresh values from an effect without re-triggering it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;useEvent&lt;/code&gt; covers the wider surface.&lt;/strong&gt; Passing a stable handler to a memoized child, an imperative widget, a WebSocket wrapper, or a third-party SDK — all things &lt;code&gt;useEffectEvent&lt;/code&gt;'s linter will reject — are precisely what a userland &lt;code&gt;useEvent&lt;/code&gt; is for. The trade-off is that the wider surface includes the render-time foot-gun above, and &lt;em&gt;you&lt;/em&gt; hold the discipline instead of the linter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;They coexist fine.&lt;/strong&gt; Use &lt;code&gt;useEffectEvent&lt;/code&gt; inside effects on React 19.2+, and &lt;code&gt;useEvent&lt;/code&gt; for stable identity across component boundaries — or use &lt;code&gt;useEvent&lt;/code&gt; everywhere below 19.2, where &lt;code&gt;useEffectEvent&lt;/code&gt; doesn't exist.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A Handler Prop That Doesn't Break React.memo
&lt;/h3&gt;

&lt;p&gt;The classic list-row scenario — a memoized row re-renders anyway because the parent recreates &lt;code&gt;onSelect&lt;/code&gt; each render:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;memo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Row&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onSelect&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;RowProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;onSelect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"row"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;List&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Item&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setSelected&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&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="nx"&gt;handleSelect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useEvent&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;// reads latest `selected`, no dep array to maintain&lt;/span&gt;
    &lt;span class="nf"&gt;setSelected&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;id&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="nx"&gt;selected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Row&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;item&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;onSelect&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleSelect&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;handleSelect&lt;/code&gt; is the same reference on every render, so &lt;code&gt;React.memo&lt;/code&gt; actually memoizes. With &lt;code&gt;useCallback&lt;/code&gt; you'd either list &lt;code&gt;selected&lt;/code&gt; (identity churns, memo defeated) or use the functional-update form everywhere (fine here, impossible once the handler reads two pieces of state).&lt;/p&gt;

&lt;h3&gt;
  
  
  Subscriptions That Never Re-Subscribe
&lt;/h3&gt;

&lt;p&gt;WebSockets, &lt;code&gt;EventSource&lt;/code&gt;, SDKs — anywhere tearing down a connection just because a closure went stale is embarrassing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;usePriceFeed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setPrice&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&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;onMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useEvent&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MessageEvent&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="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;setPrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// latest threshold, always&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ws&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;WebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`wss://feed.example.com/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;ws&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;message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onMessage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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="nx"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onMessage&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// reconnects only when symbol changes&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;price&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;The socket reconnects when &lt;code&gt;symbol&lt;/code&gt; changes — a real reason — and never when &lt;code&gt;threshold&lt;/code&gt; does. Note that for plain DOM targets, &lt;code&gt;useEventListener&lt;/code&gt; already does this internally (it wraps your handler in &lt;code&gt;useLatest&lt;/code&gt;), so you only need &lt;code&gt;useEvent&lt;/code&gt; when &lt;em&gt;you&lt;/em&gt; own the subscription plumbing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timers — or Just Use the Library's
&lt;/h3&gt;

&lt;p&gt;The heartbeat example above is common enough that &lt;code&gt;@reactuses/core&lt;/code&gt; ships it solved: &lt;code&gt;useInterval&lt;/code&gt; keeps your callback fresh without restarting the timer — and its own implementation is built on &lt;code&gt;useEvent&lt;/code&gt; and &lt;code&gt;useLatest&lt;/code&gt;. Same story for &lt;code&gt;useTimeout&lt;/code&gt;, &lt;code&gt;useDebounceFn&lt;/code&gt;, and &lt;code&gt;useThrottleFn&lt;/code&gt;: the stale-closure protection is baked in, so check whether the hook you're about to build already exists before wiring &lt;code&gt;useEvent&lt;/code&gt; yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stable Callbacks for Imperative Widgets
&lt;/h3&gt;

&lt;p&gt;Chart libraries, map SDKs, and editors typically take handlers at construction time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Editor&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;docId&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;docId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;dirty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setDirty&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleSave&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useEvent&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;saveDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;docId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// latest docId&lt;/span&gt;
    &lt;span class="nf"&gt;setDirty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;editor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createEditor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#mount&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="na"&gt;onSave&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;handleSave&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy&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="nx"&gt;handleSave&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// stable → editor created exactly once&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"mount"&lt;/span&gt; &lt;span class="na"&gt;data-dirty&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;dirty&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Recreating a heavyweight editor because &lt;code&gt;docId&lt;/code&gt; changed identity in a closure is exactly the kind of waste &lt;code&gt;useEvent&lt;/code&gt; deletes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Rules
&lt;/h2&gt;

&lt;p&gt;Two, and they're both consequences of the ref timing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Don't call the returned function during render.&lt;/strong&gt; It's for event handlers, effects, timers, callbacks — things that fire after commit. During render, the ref may still point at the previous closure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't use it to lie to yourself about effects.&lt;/strong&gt; If an effect genuinely &lt;em&gt;should&lt;/em&gt; re-run when a value changes (a query refetch when filters change, say), wrapping the logic in &lt;code&gt;useEvent&lt;/code&gt; to silence the linter buries a real dependency. &lt;code&gt;useEvent&lt;/code&gt; is for "read the latest, don't re-fire"; it's not a universal dependency-array mute button.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;useEvent&lt;/code&gt; returns a function with permanent identity and an always-fresh closure&lt;/strong&gt; — the two things &lt;code&gt;useCallback&lt;/code&gt; makes you choose between.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The trick is a ref updated in a layout effect&lt;/strong&gt; plus a once-memoized wrapper that reads the ref at call time. &lt;code&gt;useIsomorphicLayoutEffect&lt;/code&gt; keeps it SSR-safe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;useCallback&lt;/code&gt; for render-time values, &lt;code&gt;useEvent&lt;/code&gt; for fired handlers.&lt;/strong&gt; Never call a &lt;code&gt;useEvent&lt;/code&gt; function during render.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On React 19.2+, &lt;code&gt;useEffectEvent&lt;/code&gt; covers the inside-an-effect case&lt;/strong&gt; with linter enforcement; &lt;code&gt;useEvent&lt;/code&gt; covers stable handlers passed across component and library boundaries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the library first&lt;/strong&gt; — &lt;code&gt;useEventListener&lt;/code&gt;, &lt;code&gt;useInterval&lt;/code&gt;, &lt;code&gt;useDebounceFn&lt;/code&gt; and friends already ship with stale-closure protection built in.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Grab it from &lt;a href="https://reactuse.com/effect/useevent/" rel="noopener noreferrer"&gt;&lt;code&gt;@reactuses/core&lt;/code&gt;&lt;/a&gt; and retire the dependency-array whack-a-mole.&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
