Connecting your SPA's client state to WebMCP: reactive hooks and stores Developer Cristian Moreno built a React and Zustand demo showing how to register WebMCP tools in single-page apps so they survive route changes without re-registering on every render. The approach treats a tool as a contract (name, schema, description) registered once per component lifetime, with live state read inside execute at call time; in headless Chrome runs, component commits rose from 1 to 6 while registerTool() calls stayed flat, with zero aborts and five toolchange events. The demo also documents two failure modes: a naive hook that re-registers on every render and a stale-closure pattern that returns outdated state. In the first post in this series https://dev.to/khriztianmoreno/webmcp-why-agents-shouldnt-have-to-guess-how-your-ui-works-1af1 , the tools were registered once, at page load, against a store written in vanilla JavaScript. In the second https://dev.to/khriztianmoreno/semantic-markup-for-ai-turn-your-forms-into-webmcp-tools-without-javascript-1nod , there wasn't even any JavaScript: the tools were HTML forms. Both demos have something in common that your app probably doesn't: nothing mounts or unmounts. The page loads, the tools exist, end of story. A real app doesn't look like that. It's an SPA: components that mount and unmount with every route change, state living in a store Zustand, Redux, Pinia, signals, whatever you use , and a framework that re-renders whenever that state changes. The obvious question is where you register tools in that world. The obvious answer "in a useEffect , of course" hides two traps that took me a while to name: tools that re-register on every render, and tools that answer with state from three minutes ago. This post is about the idea that gets you out of both: a WebMCP tool is a contract name + schema + description , not a snapshot of state. You register it once per component lifetime, you tie its unregistration to the unmount, and you read live state inside execute , at call time. That way, a state change never re-registers anything. I built a demo in the same flight domain, now in React with a Zustand store. The code is in demos/webmcp-react-state https://github.com/khriztianmoreno/khriztianmoreno.dev/tree/main/demos/webmcp-react-state the README https://github.com/khriztianmoreno/khriztianmoreno.dev/blob/main/demos/webmcp-react-state/README.md explains how to run it . Like the previous ones, it has no build step: React, Zustand, and htm https://github.com/developit/htm JSX without a transform load from a CDN. Your browser doesn't support embedded video — clone the demo https://github.com/khriztianmoreno/khriztianmoreno.dev/tree/main/demos/webmcp-react-state and run it locally instead. The video shows the full run: renders climbing while registrations stay put, the naive hook's counters taking off, and StrictMode's unhandled rejections appearing in the page. The app is a trip cart: BOG → MAD flight results, an "Add" button on each one, and a cart with passengers and a subtotal. The state lives in a Zustand store. The