<?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: Stephen Bullocks</title>
    <description>The latest articles on DEV Community by Stephen Bullocks (@sbullocks).</description>
    <link>https://dev.to/sbullocks</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%2F3997417%2Fe38b719e-72fe-49a2-b805-966383407727.png</url>
      <title>DEV Community: Stephen Bullocks</title>
      <link>https://dev.to/sbullocks</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sbullocks"/>
    <language>en</language>
    <item>
      <title>I got tired of rewriting the same AI boilerplate so I built a library to fix it</title>
      <dc:creator>Stephen Bullocks</dc:creator>
      <pubDate>Mon, 22 Jun 2026 18:47:33 +0000</pubDate>
      <link>https://dev.to/sbullocks/i-got-tired-of-rewriting-the-same-ai-boilerplate-so-i-built-a-library-to-fix-it-4ff8</link>
      <guid>https://dev.to/sbullocks/i-got-tired-of-rewriting-the-same-ai-boilerplate-so-i-built-a-library-to-fix-it-4ff8</guid>
      <description>&lt;p&gt;Every time I added AI to a React app, I rewrote the same 200+ lines. Streaming loop. Manual message history. Tool call orchestration. Error handling. setIsLoading(false) only if I remembered.&lt;/p&gt;

&lt;p&gt;After the third project I stopped and asked: why is nobody solving this the way RTK Query solved REST APIs?&lt;/p&gt;

&lt;p&gt;So I built Strand (&lt;a href="https://github.com/strand-js/strand" rel="noopener noreferrer"&gt;https://github.com/strand-js/strand&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Before&lt;/p&gt;

&lt;p&gt;&lt;code&gt;const [messages, setMessages] = useState([])&lt;br&gt;
const [isLoading, setIsLoading] = useState(false)&lt;/code&gt;&lt;br&gt;
&lt;code&gt;async function send(text) {&lt;br&gt;
  setIsLoading(true)&lt;br&gt;
   manually stream tokens&lt;br&gt;
   manually detect tool calls&lt;br&gt;
   manually loop until done&lt;br&gt;
   setIsLoading(false) only if you remembered&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;After&lt;/p&gt;

&lt;p&gt;&lt;code&gt;const { messages, send, isPending, isStreaming, cancel } = useConversation({&lt;br&gt;
  system: 'You are a helpful assistant.',&lt;br&gt;
})&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Streaming, history, tool calls, cancellation, retry; all handled.&lt;/p&gt;

&lt;p&gt;The thing nobody else has: useToolCall&lt;/p&gt;

&lt;p&gt;Works from ANY component; no prop drilling&lt;br&gt;
&lt;code&gt;function WeatherStatus() {&lt;br&gt;
const { status, input, output } = useToolCall('get_weather')&lt;/code&gt;&lt;br&gt;
&lt;code&gt;if (status === 'running') return &amp;lt;div&amp;gt;Checking {input?.location}…&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;br&gt;
&lt;code&gt;if (status === 'done') return &amp;lt;div&amp;gt;{output?.temp}°F&amp;lt;/div&amp;gt;&lt;br&gt;
return null&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Live tool state: pending → running → done. Its observable anywhere in your tree.&lt;/p&gt;

&lt;p&gt;Fixing the isLoading design flaw&lt;/p&gt;

&lt;p&gt;The Vercel AI SDK has 4+ open issues (&lt;a href="https://github.com/vercel/ai/issues" rel="noopener noreferrer"&gt;https://github.com/vercel/ai/issues&lt;/a&gt;) about isLoading getting stuck. The reason is architectural because "request sent" and "tokens arriving" are different states.&lt;/p&gt;

&lt;p&gt;Strand tracks four:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;const { isPending, isStreaming, isDone, error } = useConversation()&lt;br&gt;
// isPending: waiting for first token&lt;br&gt;
// isStreaming: tokens arriving&lt;br&gt;
// isDone: just completed&lt;br&gt;
// error: something failed&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Works with Anthropic, OpenAI, and Google Gemini&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npm install @strand-js/core @strand-js/react zod&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
&lt;code&gt;npm install @strand-js/anthropic  # or openai, or google&lt;br&gt;
&lt;/code&gt;&lt;br&gt;
Swap providers by changing one server import. Zero frontend changes.&lt;/p&gt;

&lt;p&gt;v0.1.8, MIT, open source.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://github.com/strand-js/strand" rel="noopener noreferrer"&gt;https://github.com/strand-js/strand&lt;/a&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
