TL;DR: I built Snapstate to move business logic out of React components and into plain TypeScript classes. The result: stores you can test without React, components that only render, and a cleaner boundary between UI and application logic.
React is excellent at rendering UI. It's less convincing as the place where the rest of the app should live.
Over time, a lot of React codebases drift into the same shape: data fetching in useEffect , business rules inside custom hooks, derived values spread across useMemo , and mutations hidden in event handlers. The app still works, but the boundaries get blurry. Logic that should be easy to test or reuse ends up coupled to render timing, hook rules, and component lifecycles.
I've written plenty of code like this myself. Snapstate came out of wanting a cleaner boundary: React for rendering, plain TypeScript classes for state and business logic.
The boundary I wanted
This isn't an argument against hooks. Hooks are a good fit for UI concerns: subscribing to browser APIs, coordinating animations, managing local component state, and composing rendering behavior.
The trouble starts when application logic moves into that same layer. A hook that fetches data, normalizes it, tracks loading and errors, coordinates retries, and exposes mutations is no longer just a React concern. It's an application service expressed in React primitives.
That has a few predictable costs. Testing usually starts with rendering infrastructure instead of the logic itself. Reuse is tied to React, even when the logic is not. And understanding the behavior means reasoning about dependency arrays, mount timing, and re-renders alongside the business rules.
I wanted a place where that logic could exist without carrying React around with it.
Why not the existing options?
... continue reading