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.
After the third project I stopped and asked: why is nobody solving this the way RTK Query solved REST APIs?
So I built Strand (https://github.com/strand-js/strand).
Before
const [messages, setMessages] = useState([])
const [isLoading, setIsLoading] = useState(false)
async function send(text) {
setIsLoading(true)
manually stream tokens
manually detect tool calls
manually loop until done
setIsLoading(false) only if you remembered
}
After
const { messages, send, isPending, isStreaming, cancel } = useConversation({
system: 'You are a helpful assistant.',
})
Streaming, history, tool calls, cancellation, retry; all handled.
The thing nobody else has: useToolCall
Works from ANY component; no prop drilling
function WeatherStatus() {
const { status, input, output } = useToolCall('get_weather')
if (status === 'running') return <div>Checking {input?.location}…</div>
if (status === 'done') return <div>{output?.temp}°F</div>
return null
}
Live tool state: pending → running → done. Its observable anywhere in your tree.
Fixing the isLoading design flaw
The Vercel AI SDK has 4+ open issues (https://github.com/vercel/ai/issues) about isLoading getting stuck. The reason is architectural because "request sent" and "tokens arriving" are different states.
Strand tracks four:
const { isPending, isStreaming, isDone, error } = useConversation()
// isPending: waiting for first token
// isStreaming: tokens arriving
// isDone: just completed
// error: something failed
Works with Anthropic, OpenAI, and Google Gemini
npm install @strand-js/core @strand-js/react zod
npm install @strand-js/anthropic # or openai, or google
Swap providers by changing one server import. Zero frontend changes.
v0.1.8, MIT, open source.
Top comments (0)