<?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: Sensational-DEV</title>
    <description>The latest articles on DEV Community by Sensational-DEV (@sensational5510).</description>
    <link>https://dev.to/sensational5510</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%2F3968612%2F5add9c75-f9ee-4612-af55-8efd36731922.jpeg</url>
      <title>DEV Community: Sensational-DEV</title>
      <link>https://dev.to/sensational5510</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sensational5510"/>
    <language>en</language>
    <item>
      <title>I rewrote 3 useEffects to use() in React 19. Here's the one I shouldn't have.</title>
      <dc:creator>Sensational-DEV</dc:creator>
      <pubDate>Mon, 08 Jun 2026 06:53:41 +0000</pubDate>
      <link>https://dev.to/sensational5510/i-rewrote-3-useeffects-to-use-in-react-19-heres-the-one-i-shouldnt-have-9db</link>
      <guid>https://dev.to/sensational5510/i-rewrote-3-useeffects-to-use-in-react-19-heres-the-one-i-shouldnt-have-9db</guid>
      <description>&lt;p&gt;The first time &lt;code&gt;use()&lt;/code&gt; clicked for me, I closed the tab on the React docs and immediately went through my codebase looking for &lt;code&gt;useEffect&lt;/code&gt; calls to delete.&lt;/p&gt;

&lt;p&gt;Three rewrites later, I reverted one. Here's what I learned about when &lt;code&gt;use()&lt;/code&gt; is the right tool — and when reaching for it is the wrong instinct.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;use()&lt;/code&gt; in React 19 lets you read the value of a promise inside a component. The component suspends while the promise resolves, and React handles the rest:&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;UserCard&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userPromise&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;userPromise&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&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;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userPromise&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="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;user&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="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;No &lt;code&gt;useEffect&lt;/code&gt;. No &lt;code&gt;useState&lt;/code&gt;. No &lt;code&gt;if (loading) return &amp;lt;Spinner /&amp;gt;&lt;/code&gt;. The component reads the value as if it were synchronous, and Suspense handles the loading state from a parent boundary.&lt;/p&gt;

&lt;p&gt;It's beautiful. It's also not a drop-in replacement for every &lt;code&gt;useEffect&lt;/code&gt; you've written.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rewrite 1: the obvious win
&lt;/h2&gt;

&lt;p&gt;This was the kind of pattern &lt;code&gt;use()&lt;/code&gt; was designed for. A server component fetches data, passes the promise (not the awaited value) to a client component, and that component reads 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="c1"&gt;// app/users/[id]/page.tsx (server component)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&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;Page&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;params&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;userPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetchUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&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="c1"&gt;// not awaited&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;UserCard&lt;/span&gt; &lt;span class="na"&gt;userPromise&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;userPromise&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="c1"&gt;// UserCard.tsx (client component)&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;use&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;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;UserCard&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userPromise&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;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userPromise&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;user&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="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;The parent server component starts the fetch and ships the promise. The client component reads it. There's a Suspense boundary somewhere above that handles the loading state.&lt;/p&gt;

&lt;p&gt;The entire &lt;code&gt;useState + useEffect + setUser&lt;/code&gt; dance I'd written for the old version evaporated. 18 lines down to 4. Lossless.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rewrite 2: the slightly less obvious win
&lt;/h2&gt;

&lt;p&gt;The second was for a config object that came from a context provider. The context's value was itself a promise (resolved lazily so the bundle didn't ship a megabyte of config), and consumers had been awkwardly &lt;code&gt;await&lt;/code&gt;ing it inside &lt;code&gt;useEffect&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="c1"&gt;// Before&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Feature&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;configPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ConfigContext&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;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setConfig&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;Config&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="nx"&gt;configPromise&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;setConfig&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;configPromise&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;config&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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;RealFeature&lt;/span&gt; &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;config&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="c1"&gt;// After&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Feature&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;configPromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ConfigContext&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;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;configPromise&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;RealFeature&lt;/span&gt; &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;config&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;use()&lt;/code&gt; works seamlessly with context — pass a promise through context, read the value with &lt;code&gt;use()&lt;/code&gt; anywhere downstream. The component naturally suspends until the promise resolves.&lt;/p&gt;

&lt;p&gt;Four lines. The &lt;code&gt;if (!config) return null&lt;/code&gt; early return is gone, which means downstream types stop being &lt;code&gt;Config | null&lt;/code&gt; and start being &lt;code&gt;Config&lt;/code&gt;. Type-level win on top of the code-level win.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rewrite 3: the one I reverted
&lt;/h2&gt;

&lt;p&gt;Flush with confidence, I went after a &lt;code&gt;useEffect&lt;/code&gt; that polled a status endpoint every 5 seconds:&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;// Before&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;JobStatus&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="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;Status&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="s1"&gt;pending&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;let&lt;/span&gt; &lt;span class="nx"&gt;cancelled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tick&lt;/span&gt; &lt;span class="o"&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;next&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;fetchJobStatus&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="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;cancelled&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;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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;cancelled&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tick&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="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nf"&gt;tick&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;cancelled&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="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;jobId&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;StatusPill&lt;/span&gt; &lt;span class="na"&gt;status&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="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;My first attempt was to wrap each poll in a promise and read it with &lt;code&gt;use()&lt;/code&gt;. It "worked" for the first fetch. Then I needed to re-poll. And cancel on unmount. And re-trigger when &lt;code&gt;jobId&lt;/code&gt; changed.&lt;/p&gt;

&lt;p&gt;Within 20 minutes I had reinvented &lt;code&gt;useEffect&lt;/code&gt; poorly — a custom hook that kept track of the current promise, swapped it for a new one on each interval, and tore everything down on unmount. The code was longer, less readable, and had subtle bugs around the cleanup path.&lt;/p&gt;

&lt;p&gt;I reverted. The &lt;code&gt;useEffect&lt;/code&gt; version was correct and obvious.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule I derived
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;use()&lt;/code&gt; is for promises that &lt;strong&gt;resolve once and don't need cancellation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's it. That's the rule.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Server-component-to-client-component data handoff: one resolution, never cancelled. ✅&lt;/li&gt;
&lt;li&gt;Lazy context value: one resolution, never cancelled. ✅&lt;/li&gt;
&lt;li&gt;Polling, websockets, subscriptions, debounced search: multiple resolutions, may need cancellation. ❌&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ones in the ❌ column still want &lt;code&gt;useEffect&lt;/code&gt; — or a purpose-built hook like SWR or TanStack Query for the network-y ones. &lt;code&gt;use()&lt;/code&gt; doesn't have a cancellation API because it isn't trying to be one. It's a primitive for &lt;em&gt;reading&lt;/em&gt;, not for &lt;em&gt;managing&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The meta lesson
&lt;/h2&gt;

&lt;p&gt;New React features land and they look like silver bullets for ten minutes. They're not. They're sharp tools with narrow blades.&lt;/p&gt;

&lt;p&gt;The time to reach for &lt;code&gt;use()&lt;/code&gt; is when you have a single promise sitting in the right place at the right time and you're tired of writing 18 lines of state-and-effect glue to read it. The time to &lt;em&gt;not&lt;/em&gt; reach for &lt;code&gt;use()&lt;/code&gt; is when you've got lifecycle work — polling, subscriptions, cancellation, anything that lives over time.&lt;/p&gt;

&lt;p&gt;Learn the shape of each new primitive. Don't refactor at scale until you've shipped one example end-to-end. And keep a &lt;code&gt;git revert&lt;/code&gt; in your pocket — you'll need it the day after you discover something new.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If this helped, you can &lt;a href="https://buymeacoffee.com/budu_dev" rel="noopener noreferrer"&gt;buy me a coffee&lt;/a&gt;. It keeps the writing coming.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>3 TypeScript patterns I keep stealing from open-source codebases</title>
      <dc:creator>Sensational-DEV</dc:creator>
      <pubDate>Mon, 08 Jun 2026 05:53:04 +0000</pubDate>
      <link>https://dev.to/sensational5510/3-typescript-patterns-i-keep-stealing-from-open-source-codebases-32be</link>
      <guid>https://dev.to/sensational5510/3-typescript-patterns-i-keep-stealing-from-open-source-codebases-32be</guid>
      <description>&lt;p&gt;Every few months I find a TypeScript pattern in someone's open-source repo that I instantly bring back to my own code. It's the cheapest form of getting better — read code that's been read by hundreds of contributors and find the patterns that survived.&lt;/p&gt;

&lt;p&gt;Here are three I keep reaching for in 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;satisfies&lt;/code&gt; for catch-typos-at-compile-time configs
&lt;/h2&gt;

&lt;p&gt;You know the routine — a config object that holds, say, all your route paths, or all your event names. You want autocomplete when you reference one, but you also want TS to scream if you fat-finger a key.&lt;/p&gt;

&lt;p&gt;The old way:&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;type&lt;/span&gt; &lt;span class="nx"&gt;RouteKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;home&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;blog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Routes&lt;/span&gt; &lt;span class="o"&gt;=&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="nx"&gt;RouteKey&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;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Routes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;home&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;blog&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/blog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/profile&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;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;home&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// type is `string` — autocomplete works, but you lose the literal&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;satisfies&lt;/code&gt; way:&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;routes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;home&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;blog&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/blog&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/profile&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;satisfies&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;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;home&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// type is the literal '/', not just string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;satisfies&lt;/code&gt; checks the shape but preserves the &lt;em&gt;specific&lt;/em&gt; inferred types of every key and value. If I refer to &lt;code&gt;routes.home&lt;/code&gt;, TS knows the value is &lt;code&gt;'/'&lt;/code&gt;, not just &lt;code&gt;string&lt;/code&gt;. Useful when those literals drive other types downstream:&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;type&lt;/span&gt; &lt;span class="nx"&gt;Path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="c1"&gt;// Path is '/' | '/blog' | '/profile' — not string&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I stole this from a Remix codebase last year. It's in every project I touch now.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Branded types to stop ID confusion
&lt;/h2&gt;

&lt;p&gt;Ever passed &lt;code&gt;userId&lt;/code&gt; where &lt;code&gt;postId&lt;/code&gt; was expected? TypeScript won't catch it, because both are strings.&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;function&lt;/span&gt; &lt;span class="nf"&gt;getUser&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="p"&gt;{&lt;/span&gt; &lt;span class="cm"&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;getPost&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="p"&gt;{&lt;/span&gt; &lt;span class="cm"&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;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;u_abc&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;postId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;p_xyz&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// no error — the bug ships&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Brand them and the bug becomes unrepresentable:&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;type&lt;/span&gt; &lt;span class="nx"&gt;Brand&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="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;B&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;__brand&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;B&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;UserId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Brand&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UserId&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="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PostId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Brand&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PostId&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;asUserId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&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;UserId&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;s&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;UserId&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;asPostId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&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;PostId&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;s&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;PostId&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;getUser&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="nx"&gt;UserId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&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;getPost&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="nx"&gt;PostId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&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;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;asUserId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;u_abc&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;postId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;asPostId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;p_xyz&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Error: 'PostId' is not assignable to 'UserId'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The branding is a zero-cost lie — &lt;code&gt;UserId&lt;/code&gt; is really just &lt;code&gt;string&lt;/code&gt; at runtime — but the compiler treats them as different types. The &lt;code&gt;asUserId&lt;/code&gt; helper is the only blessed entry point, so all the unsafe casting happens in one place you can audit.&lt;/p&gt;

&lt;p&gt;I picked this up from a fintech repo where mixing up &lt;code&gt;OrderId&lt;/code&gt; and &lt;code&gt;TradeId&lt;/code&gt; would have been a five-figure mistake. For a side project it's overkill. For anything touching money or user data, I reach for it instantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Discriminated unions with a &lt;code&gt;kind&lt;/code&gt; field
&lt;/h2&gt;

&lt;p&gt;This isn't new, but I use it more than any other pattern on this list, so it belongs.&lt;/p&gt;

&lt;p&gt;State that can be in one of several modes — loading / success / error, draft / published / archived, free / pro / enterprise — should be a discriminated union, not three optional booleans.&lt;/p&gt;

&lt;p&gt;The painful way:&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;FetchState&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;data&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="nl"&gt;error&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;isLoading&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&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="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// might still be loading? unclear&lt;/span&gt;
  &lt;span class="c1"&gt;// might have a stale error? also unclear&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The clean way:&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;type&lt;/span&gt; &lt;span class="nx"&gt;FetchState&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="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idle&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="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loading&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="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;data&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;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;error&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FetchState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&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="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;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;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loading&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Spinner&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;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;success&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Profile&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;state&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="sr"&gt;/&amp;gt;;     /&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;typed&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ErrorMessage&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;state&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="sr"&gt;/&amp;gt;; /&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;typed&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;Inside each &lt;code&gt;case&lt;/code&gt; block, TS narrows the type for you. &lt;code&gt;state.data&lt;/code&gt; exists in &lt;code&gt;'success'&lt;/code&gt; and nowhere else. You can't accidentally read it from &lt;code&gt;'loading'&lt;/code&gt; because it isn't on that variant.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;kind&lt;/code&gt; field name is convention. Some teams use &lt;code&gt;type&lt;/code&gt;, some use &lt;code&gt;status&lt;/code&gt;. Pick one, use it everywhere — switching the discriminator name across files makes every code review harder than it needs to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd skip
&lt;/h2&gt;

&lt;p&gt;Decorator-heavy patterns. Stage-3 decorators are stable in TS 5+, but every project I've used them in eventually hit a corner where they didn't compose well with the framework's lifecycle. I've gone back to plain functions and higher-order helpers and never missed the magic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The meta lesson
&lt;/h2&gt;

&lt;p&gt;Reading open-source isn't just for inspiration. It's professional development without the bootcamp tuition. Every pattern in this list survived hundreds of contributors making small calls — that's a stronger filter than my own taste alone.&lt;/p&gt;

&lt;p&gt;If you haven't already, pick one TypeScript codebase you admire and read 30 minutes of source this week. Try to spot a pattern you don't already use. Steal it. Use it on Monday.&lt;/p&gt;

&lt;p&gt;That's how you get better — one stolen pattern at a time.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If this helped, you can &lt;a href="https://buymeacoffee.com/budu_dev" rel="noopener noreferrer"&gt;buy me a coffee&lt;/a&gt;. It keeps the writing coming.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>5 Tailwind v4 features I wish I'd known earlier</title>
      <dc:creator>Sensational-DEV</dc:creator>
      <pubDate>Sat, 06 Jun 2026 16:41:24 +0000</pubDate>
      <link>https://dev.to/sensational5510/5-tailwind-v4-features-i-wish-id-known-earlier-21ia</link>
      <guid>https://dev.to/sensational5510/5-tailwind-v4-features-i-wish-id-known-earlier-21ia</guid>
      <description>&lt;p&gt;I migrated a project to Tailwind v4 last week. The first hour was disorienting — half of what I knew didn't apply. Two days in, I'm faster than I was on v3.&lt;/p&gt;

&lt;p&gt;These are the five features that made the difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;@theme&lt;/code&gt; is the new config
&lt;/h2&gt;

&lt;p&gt;In v3, every theme value lived in a JavaScript object inside &lt;code&gt;tailwind.config.js&lt;/code&gt;. Custom colors, fonts, radii — all there.&lt;/p&gt;

&lt;p&gt;In v4, the config moved into your CSS:&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="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;@theme&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--color-background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#0a0a0a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fafafa&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#06b6d4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--font-sans&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"Inter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;system-ui&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&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;These become real CSS custom properties. &lt;code&gt;--color-accent&lt;/code&gt; works in two places at once: in raw CSS (&lt;code&gt;color: var(--color-accent)&lt;/code&gt;) and in Tailwind classes (&lt;code&gt;bg-accent&lt;/code&gt;, &lt;code&gt;text-accent&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;I don't miss &lt;code&gt;tailwind.config.js&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;code&gt;@custom-variant&lt;/code&gt; for class-based dark mode
&lt;/h2&gt;

&lt;p&gt;Built-in dark mode in v4 keys off the &lt;code&gt;prefers-color-scheme&lt;/code&gt; media query. If you want class-based dark mode (so users can toggle themes manually), v3 had a config option. V4 wants 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="k"&gt;@custom-variant&lt;/span&gt; &lt;span class="n"&gt;dark&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(.&lt;/span&gt;&lt;span class="n"&gt;dark&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dark&lt;/span&gt; &lt;span class="err"&gt;*&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then everywhere in your code, &lt;code&gt;dark:bg-card&lt;/code&gt; activates when &lt;code&gt;.dark&lt;/code&gt; class is on &lt;code&gt;&amp;lt;html&amp;gt;&lt;/code&gt;. Pair it with a CSS override block:&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;.dark&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--color-background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#0a0a0a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--color-foreground&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fafafa&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;Toggle by adding/removing the class. No more config-file dance.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. &lt;code&gt;color-mix()&lt;/code&gt; for accent-aware effects
&lt;/h2&gt;

&lt;p&gt;This isn't strictly a Tailwind feature — it's CSS — but Tailwind v4 made it feel native by encouraging CSS-first variables.&lt;/p&gt;

&lt;p&gt;The use case: a glow effect that tracks your accent color, regardless of theme.&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;.glow&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;box-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;6px&lt;/span&gt; &lt;span class="m"&gt;20px&lt;/span&gt; &lt;span class="m"&gt;-6px&lt;/span&gt; &lt;span class="n"&gt;color-mix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt; &lt;span class="n"&gt;srgb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--color-accent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="m"&gt;55%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;transparent&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;Change &lt;code&gt;--color-accent&lt;/code&gt;, the glow color follows. Change theme (dark to light), the glow stays accent-aware. One declaration, two-mode behavior.&lt;/p&gt;

&lt;p&gt;I use this everywhere — buttons, cards, focus rings. It's a tiny detail that polishes a whole UI.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Container queries without a plugin
&lt;/h2&gt;

&lt;p&gt;In v3, &lt;code&gt;@tailwindcss/container-queries&lt;/code&gt; was a plugin you installed. In v4, container queries are built in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&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;"@container"&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;"grid grid-cols-1 @md:grid-cols-2 @lg:grid-cols-3"&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;/* responds to container width, not viewport */&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The grid switches columns based on the &lt;strong&gt;parent's&lt;/strong&gt; width, not the window. If you've ever built a dashboard widget that's used at different sizes in different layouts, this is the cleanest solution I've found.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. CSS-first config means less plumbing
&lt;/h2&gt;

&lt;p&gt;I had a folder for Tailwind plugin configs in v3 projects. In v4 it doesn't exist.&lt;/p&gt;

&lt;p&gt;Want a custom utility?&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="k"&gt;@utility&lt;/span&gt; &lt;span class="n"&gt;container-tight&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;36rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;margin-inline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&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;Use it: &lt;code&gt;&amp;lt;div className="container-tight"&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Want a custom variant?&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="k"&gt;@custom-variant&lt;/span&gt; &lt;span class="n"&gt;hocus&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;hover&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use it: &lt;code&gt;&amp;lt;button className="hocus:bg-accent"&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;No more &lt;code&gt;plugin(function({ addUtilities, addVariant }) { … })&lt;/code&gt;. The mental model collapses into &lt;em&gt;"CSS with extra rules"&lt;/em&gt;, which is what Tailwind probably should have been all along.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd skip
&lt;/h2&gt;

&lt;p&gt;Honestly, the &lt;code&gt;@apply&lt;/code&gt; directive. It exists, it works, but I find that with CSS variables driving everything, I reach for it less. Direct class composition wins for clarity 80% of the time, and the other 20% I reach for &lt;code&gt;@layer components { … }&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migration tip
&lt;/h2&gt;

&lt;p&gt;The official upgrade tool (&lt;code&gt;npx @tailwindcss/upgrade@latest&lt;/code&gt;) handles 90% of the rewrite. The 10% it doesn't: dark-mode setup (you have to choose: media query or class), and any custom plugins (rewrite them as &lt;code&gt;@utility&lt;/code&gt; or &lt;code&gt;@custom-variant&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;I spent 30 minutes on the upgrade tool, then another 90 minutes on manual cleanup. Net result: less code, the same UI, and a project that's easier to onboard.&lt;/p&gt;

&lt;p&gt;If you've been holding off because v3 works fine — try v4 on a small project first. The mental shift is real, but it's a one-time cost and the daily ergonomics are worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  One last thing
&lt;/h2&gt;

&lt;p&gt;The biggest win I didn't expect: &lt;code&gt;@theme&lt;/code&gt; makes design tokens &lt;strong&gt;visible&lt;/strong&gt;. In v3 they lived inside a JS object that nobody on the team looked at. In v4 they're at the top of your &lt;code&gt;globals.css&lt;/code&gt;, next to the imports. New contributors find them in their first 30 seconds in the file.&lt;/p&gt;

&lt;p&gt;That's not a Tailwind feature. That's a CSS architecture decision that v4 quietly forces on you. And it's a good one.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If this helped, you can &lt;a href="https://buymeacoffee.com/budu_dev" rel="noopener noreferrer"&gt;buy me a coffee&lt;/a&gt;. It keeps the writing coming.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tailwindcss</category>
      <category>css</category>
      <category>frontend</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I gave Claude an API to post to my dev blog (and the bcrypt hash broke in 2 different places)</title>
      <dc:creator>Sensational-DEV</dc:creator>
      <pubDate>Fri, 05 Jun 2026 10:05:14 +0000</pubDate>
      <link>https://dev.to/sensational5510/i-gave-claude-an-api-to-post-to-my-dev-blog-and-the-bcrypt-hash-broke-in-2-different-places-18mo</link>
      <guid>https://dev.to/sensational5510/i-gave-claude-an-api-to-post-to-my-dev-blog-and-the-bcrypt-hash-broke-in-2-different-places-18mo</guid>
      <description>&lt;p&gt;It started with a side project: a personal blog. I wanted somewhere to write that wasn't a hosted platform, with the dark-mode-by-default and the URL structure and the editor I'd been craving.&lt;/p&gt;

&lt;p&gt;Then halfway through building it, a thought:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What if I could just tell Claude &lt;em&gt;"publish a post about X"&lt;/em&gt; and it actually shows up on my blog?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;48 hours later, here we are.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental model
&lt;/h2&gt;

&lt;p&gt;There are two ways to write on the blog:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Web admin&lt;/strong&gt; — a split-view markdown editor with live preview, behind HTTP Basic Auth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API endpoint&lt;/strong&gt; — &lt;code&gt;POST /api/ai/posts&lt;/code&gt; with an &lt;code&gt;X-API-Key&lt;/code&gt; header&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Path #2 is the new thing. The key idea: every Claude Code session I run from the terminal already has the API key in my env. So I can just do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3210/api/ai/posts &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: &lt;/span&gt;&lt;span class="nv"&gt;$BLOG_AI_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"title":"...","body_markdown":"...","status":"draft"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a draft (or published post) appears on the blog. No browser. No copy-paste.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Next.js 16 (App Router) + React 19 + TypeScript
Tailwind v4 (dark-first)
File-based JSON store (will swap to Postgres later)
Bcrypt for the API key
Zod for input validation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm intentionally not using a database yet. A &lt;code&gt;.data/posts.json&lt;/code&gt; file works fine while I'm the only writer. Trading 30 lines of complexity for &lt;em&gt;"I'll migrate when it matters."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The endpoint
&lt;/h2&gt;

&lt;p&gt;The handler lives at &lt;code&gt;app/api/ai/posts/route.ts&lt;/code&gt;. It exports four functions for four verbs:&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* list */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&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;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* create */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&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;PUT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* update */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&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;DELETE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* delete */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The shared first step in each is auth:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;requireAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;apiKeyConfigured&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="nf"&gt;jsonError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;503&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 key not configured&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;ok&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;verifyApiKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&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-api-key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ok&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;jsonError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid or missing X-API-Key&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;null&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;Then the verb-specific logic does the obvious thing — parse JSON, validate with zod, call the store, return JSON back.&lt;/p&gt;

&lt;p&gt;The clever bit is in the store: when Claude POSTs a new post, the slug is auto-generated from the title. If that slug is taken, it appends &lt;code&gt;-2&lt;/code&gt;, &lt;code&gt;-3&lt;/code&gt;, etc. So Claude can call POST repeatedly with similar titles without worrying about collisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bcrypt gotcha (and the second one I didn't see coming)
&lt;/h2&gt;

&lt;p&gt;This took me 30 minutes to figure out and I want it documented somewhere so I never fall for it again.&lt;/p&gt;

&lt;p&gt;I generated an API key, hashed it with &lt;code&gt;bcryptjs&lt;/code&gt;, dropped both into &lt;code&gt;.env.local&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;BLOG_AI_API_KEY_HASH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2b$10$yNATBgF&lt;/span&gt;&lt;span class="s2"&gt;..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restarted the server. Called the endpoint. Got &lt;code&gt;503: API key not configured&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's the error I throw when &lt;code&gt;process.env.BLOG_AI_API_KEY_HASH&lt;/code&gt; is undefined.&lt;/p&gt;

&lt;p&gt;But the env file has it. What gives?&lt;/p&gt;

&lt;p&gt;Turns out Next.js's &lt;code&gt;@next/env&lt;/code&gt; loader does &lt;strong&gt;variable expansion&lt;/strong&gt; inside double-quoted values. Bcrypt hashes start with &lt;code&gt;$2b$10$&lt;/code&gt; and contain more &lt;code&gt;$&lt;/code&gt; characters. The loader sees &lt;code&gt;$2b&lt;/code&gt;, &lt;code&gt;$10&lt;/code&gt; — those look like variable references to it. They don't exist as actual env vars, so they expand to empty strings, and your hash collapses to garbage.&lt;/p&gt;

&lt;p&gt;I tried single quotes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;BLOG_AI_API_KEY_HASH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'$2b$10$yNATBgF...'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same problem. Next.js's loader doesn't respect single-quoting the same way standard dotenv does — that's the second gotcha. In a vanilla &lt;code&gt;dotenv.config()&lt;/code&gt; call, single quotes prevent expansion. In Next.js they don't.&lt;/p&gt;

&lt;p&gt;Two fixes that actually work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Escape the dollars&lt;/strong&gt;: &lt;code&gt;BLOG_AI_API_KEY_HASH="\$2b\$10\$..."&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skip the hash for local dev&lt;/strong&gt;: store the raw key in &lt;code&gt;BLOG_AI_API_KEY&lt;/code&gt;, do a constant-time string compare. The &lt;code&gt;.env.local&lt;/code&gt; is gitignored anyway.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I went with option 2 locally and option 1 (via Vercel's env-var UI, which doesn't interpret &lt;code&gt;$&lt;/code&gt;) in production. The verifier is bilingual:&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyApiKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawKey&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;|&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="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;rawKey&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BLOG_AI_API_KEY_HASH&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;hash&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;bcrypt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hash&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;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BLOG_AI_API_KEY&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;raw&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;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;raw&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefers the hash. Falls back to raw. Works everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I actually use it
&lt;/h2&gt;

&lt;p&gt;Honestly, the most common use case so far is during the writing loop itself. I'll be in the terminal with Claude Code, talking through an idea. When it's good, I ask Claude to draft it and POST it as a draft. Then I open the admin in my browser and finish it in the split-view editor.&lt;/p&gt;

&lt;p&gt;That's the whole thing: AI handles the rough cut, I handle the edit. Same as code review, but for prose.&lt;/p&gt;

&lt;p&gt;The other surprisingly nice flow is updates. Halfway through writing a post I'll realize I want a fourth section. I describe it. Claude PUTs the update. I refresh the editor — there it is. The AI is operating &lt;em&gt;inside&lt;/em&gt; my draft, not next to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dev.to cross-posting&lt;/strong&gt;: a button in the editor that POSTs to Dev.to with &lt;code&gt;canonical_url&lt;/code&gt; pointing back to my blog. SEO win.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migrating to Postgres&lt;/strong&gt;: when the file store becomes annoying — probably around 50 posts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An &lt;code&gt;/api/ai/profile&lt;/code&gt; endpoint&lt;/strong&gt;: so I can update the about page from Claude too. Right now I have to edit a JSON file or use the UI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image upload via API&lt;/strong&gt;: so Claude can attach a cover image without me having to upload it separately.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;The architecture is uncomplicated enough to lift in an afternoon:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Next.js 16 API route with zod validation&lt;/li&gt;
&lt;li&gt;Bcrypt-or-raw key check (mind the dollar signs)&lt;/li&gt;
&lt;li&gt;JSON file or DB on the back&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you build something similar — or if you've found a saner way to handle bcrypt hashes in &lt;code&gt;.env&lt;/code&gt; — I want to hear about it. The comments are open.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If this helped, you can &lt;a href="https://buymeacoffee.com/budu_dev" rel="noopener noreferrer"&gt;buy me a coffee&lt;/a&gt;. It keeps the writing coming.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>nextjs</category>
      <category>claude</category>
    </item>
    <item>
      <title>5 AI Tools I Use Every Day as a Developer in 2026 (And 3 I Quit)</title>
      <dc:creator>Sensational-DEV</dc:creator>
      <pubDate>Thu, 04 Jun 2026 16:28:28 +0000</pubDate>
      <link>https://dev.to/sensational5510/5-ai-tools-i-use-every-day-as-a-developer-in-2026-and-3-i-quit-158i</link>
      <guid>https://dev.to/sensational5510/5-ai-tools-i-use-every-day-as-a-developer-in-2026-and-3-i-quit-158i</guid>
      <description>&lt;p&gt;It's 2026, and if you're still coding the way you did in 2023, you're working twice as hard for half the output.&lt;/p&gt;

&lt;p&gt;I'm a frontend developer, and over the past year I've tested over 30 AI tools. Some saved me 10+ hours a week. Others were just hype.&lt;/p&gt;

&lt;p&gt;Here's my honest stack — what stayed, what got cut, and why.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✅ The 5 I Use Every Single Day
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Claude Code (Anthropic)
&lt;/h3&gt;

&lt;p&gt;This is my main driver. Not ChatGPT, not Copilot — Claude Code.&lt;/p&gt;

&lt;p&gt;Why? It actually understands my codebase. I can say &lt;em&gt;"refactor this component to use the new design system"&lt;/em&gt; and it reads the right files, makes the changes, runs the tests. No copy-paste loop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Real refactoring, multi-file changes, debugging gnarly bugs.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Cursor
&lt;/h3&gt;

&lt;p&gt;When I need to stay in an IDE with full visual control, Cursor is still king. The Tab autocomplete alone is worth the $20/month.&lt;/p&gt;

&lt;p&gt;I use Cursor for quick edits, Claude Code for big tasks. They're not competitors — they're a combo.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. v0 by Vercel
&lt;/h3&gt;

&lt;p&gt;I stopped designing UI in Figma for prototypes. Type a prompt → get a working React component with shadcn/ui. Ship to production in minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Landing pages, dashboards, internal tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Perplexity
&lt;/h3&gt;

&lt;p&gt;Google is dead to me for technical research. Perplexity gives me sourced answers with citations. When I'm debugging a library issue, this finds the GitHub issue + Stack Overflow thread + docs in one shot.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Raycast AI
&lt;/h3&gt;

&lt;p&gt;A keystroke away from anywhere on macOS. I use it for quick translations, regex generation, and rubber-duck conversations while coding.&lt;/p&gt;




&lt;h2&gt;
  
  
  ❌ The 3 I Quit
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. GitHub Copilot
&lt;/h3&gt;

&lt;p&gt;Once Cursor's Tab model launched, Copilot felt slow and dumb. I cancelled my subscription 4 months ago and haven't missed it once.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. ChatGPT (for coding)
&lt;/h3&gt;

&lt;p&gt;Still great for writing, but for code? Claude 4.7 is on another level. Better at long context, fewer hallucinated APIs.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Generic "AI website builders"
&lt;/h3&gt;

&lt;p&gt;Tools like Framer AI, Wix AI — they look magical for 5 minutes, then you hit a wall. Real projects need real code. Stick to v0 + Cursor.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Lesson
&lt;/h2&gt;

&lt;p&gt;The winner of 2026 isn't the developer who knows the most frameworks. It's the developer who builds the best &lt;strong&gt;workflow&lt;/strong&gt; around AI.&lt;/p&gt;

&lt;p&gt;Pick 2-3 tools. Get fast with them. Ignore the rest.&lt;/p&gt;

&lt;p&gt;What's in your stack? Drop it in the comments — I'm always looking to optimize.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If this helped, follow me for more honest dev tool reviews. No sponsorships, just what actually works.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If this helped, you can &lt;a href="https://buymeacoffee.com/budu_dev" rel="noopener noreferrer"&gt;buy me a coffee&lt;/a&gt;. It keeps the writing coming.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>webdev</category>
      <category>tools</category>
    </item>
  </channel>
</rss>
