React and tosijs
A lot of developers are using React — and tosijs is very much relevant to them. The
tiny react-tosijs library is insanely simple state
management for React — and an off-ramp from React.
If you just want the state management, useTosi has useState's shape and works better:
state isn't trapped in component lifecycles, closures, or hook rules, and you can change
it from outside React — a timer, a socket handler, the browser console — and views just
follow. No reducers, no providers, no prop-drilling. If you want the off-ramp, your state
lives in tosijs as plain observable objects with no framework attached, and React
becomes just one way of looking at it.
useTosi — a useState-shaped hook
Put any object in a tosi proxy, then read it like useState via its path. It's built
on useSyncExternalStore, so it's concurrent-rendering safe.
import { tosi } from 'tosijs'
import { useTosi } from 'react-tosijs'
const { clock } = tosi({
clock: { time: new Date().toLocaleTimeString() },
})
setInterval(() => {
clock.time = new Date().toLocaleTimeString()
}, 1000)
const Clock = () => {
const [time] = useTosi('clock.time')
return <div>{time}</div>
}
useTosi returns [value, setValue] just like useState, but the state is updated
outside React and it just works. One difference: setValue takes the next value,
not an updater function — setCount(c => c + 1) would store the function itself (tosijs
state legitimately holds functions, so they are never auto-invoked).
Because the state is a plain observable object, your logic is testable without rendering anything, the console can poke it directly, and swapping a React view for a web component later doesn't touch it at all. That's the off-ramp.
reactWebComponents — web components as React components
reactWebComponents.fooBar gives you a React component that renders a <foo-bar> custom
element, so web components (e.g. from tosijs-ui) drop into React
with no wrappers — bound to the same state as your React views.
import 'tosijs-ui'
import { reactWebComponents } from 'react-tosijs'
const Markdown = reactWebComponents.tosiMd
const Doc = () => <Markdown class="doc" src="/README.md" />
On React 18 pass class (not className) to custom elements — React 18 sets props on
them as attributes verbatim; React 19 handles className on custom elements natively.
Typed paths, persistence, DevTools
All framework-free — they work whether a path is rendered by React, a web component, or nothing at all:
import { typedTosi, persist, connectDevTools } from 'react-tosijs'
type AppState = { app: { count: number; todos: { id: string; text: string }[] } }
const { useTosi } = typedTosi<AppState>()
const [text] = useTosi('app.todos[0].text') // string
const [oops] = useTosi('app.cuont') // compile error
persist('app.todos') // localStorage, coalesced writes
connectDevTools({ roots: ['app'] }) // Redux DevTools tap
Observant, not reactive
React answers "did it change?" at the subscription source and re-renders from there;
tosijs does change detection at the DOM-update seam — an unchanged bound value is a
no-op at write time. react-tosijs's internal machinery exists to pay React's toll on
React's behalf. Delete React and the toll goes with it — which is what makes it a genuine
off-ramp, not just another React-state library that deepens the commitment.
react-tosijs needs React ^18.2 or ^19 (the hook is built on
useSyncExternalStore). See react.tosijs.net for the full
guide — and angular.tosijs.net runs the same app in React
and Angular side by side, bound to one shared state, neither framework aware of the other.