ngx-tosijs
v0.9.1
Published
Insanely simple state management for Angular — and an off-ramp from Angular. Take your pick. tosijs state as Angular signals, no zones, no RxJS plumbing, and state reachable from outside the Angular tree
Maintainers
Readme
ngx-tosijs
github | npm | live demo | tosijs | react-tosijs | discord
Insanely simple state management for Angular — and an off-ramp from Angular. Take your pick.
If you just want the state management: tosiSignal gives you a writable Angular signal
bound to a path — no zones, no NgZone.run, no RxJS plumbing, no NgRx boilerplate —
and you can talk to state from outside the Angular tree: a timer, a socket handler, the
browser console, code that's never heard of Angular. If you want the off-ramp (one that
otherwise basically doesn't exist): your state and logic live in
tosijs — plain observable objects with no framework attached —
and Angular becomes just one way of viewing them. Build new functionality on a vastly
superior state management system, and migrate away from Angular as fast or as slowly
as makes sense.
See it live: angular.tosijs.net runs the same Reminders app in Angular (zoneless signals) and React (react-tosijs) side by side, bound to the same state. Neither framework knows the other exists.
The off-ramp, step by step
- Move state and logic out of components into a tosijs proxy. This works inside your existing Angular app — any version ≥ 16, zoned or zoneless, no rewrite.
- Components become thin views via
tosiSignal. Stores, dispatch, effects-classes, andasyncpipe plumbing simply stop being necessary. - Build new UI as web components (tosijs-ui, or your own
via tosijs
Component), hosted directly in Angular templates (CUSTOM_ELEMENTS_SCHEMA— Angular hosts custom elements natively, no wrappers). They bind to the same paths as your Angular views — both stay in sync automatically, because neither owns the state. - Replace remaining Angular views at your own pace. When the last one goes, delete
@angular/*from package.json. Your state, logic, and new components are untouched — they never depended on Angular in the first place.
There is no step where you maintain two sources of truth, write a sync layer, or do a big-bang rewrite. (And if your escape route passes through React territory — or vice versa — react-tosijs binds the same state from React.)
tosiSignal in two minutes
import { Component } from '@angular/core'
import { xinProxy } from 'tosijs'
import { tosiSignal } from 'ngx-tosijs'
const { clock } = xinProxy({
clock: { time: new Date().toLocaleTimeString() },
})
setInterval(() => {
clock.time = new Date().toLocaleTimeString()
}, 1000)
@Component({
selector: 'app-clock',
standalone: true,
template: `<div>{{ time() }}</div>`,
})
export class ClockComponent {
time = tosiSignal<string>('clock.time')
}The interval updates state outside Angular — no zone, no markForCheck, no
ChangeDetectorRef — and the view follows, because tosiSignal is a real Angular
signal: read it in templates, computed(), or effect(); call .set() / .update()
to write through to the shared state. Cleanup is automatic via DestroyRef (create it
in a component/service, or pass { injector }, or { manualCleanup: true } and call
.destroy() yourself).
Works zoneless (recommended — signals notify Angular's scheduler directly) and in Zone.js apps alike.
Zone.js apps: provideTosi()
For zoned apps that read tosijs proxies directly in templates (dirty checking
re-reads them every pass), provideTosi() bridges tosijs flushes into the zone so
mutations from outside Angular trigger change detection — coalesced to one zone entry
per flush:
bootstrapApplication(AppComponent, {
providers: [provideTosi()],
})Unnecessary (and harmless) in zoneless apps using tosiSignal.
Typed paths
import { typedTosi } from 'ngx-tosijs'
type AppState = {
app: { count: number; todos: { id: string; text: string }[] }
}
const { tosiSignal } = typedTosi<AppState>()
const text = tosiSignal('app.todos[0].text') // TosiSignal<string | undefined>
const oops = tosiSignal('app.cuont') // compile errorTosiPath<S> and TosiPathValue<S, P> are exported for building your own typed helpers.
Persistence & DevTools
Framework-free, identical to react-tosijs:
import { persist, connectDevTools } from 'ngx-tosijs'
const stop = persist('app.todos') // localStorage sync, coalesced writes
const disconnect = connectDevTools({ roots: ['app'] }) // Redux DevTools tapDurable state outlives code: if the shape of a persisted value changes between
releases, bump the key.
Compatibility
- Angular
>=16 <23— the floor is API-based (signals +DestroyRefarrived in 16); the suite exercises Angular 22 (zoneless, JIT-compiled without the CLI). - tosijs
^1.0.6— the library usestosiPath/tosiValuewhen available (tosijs ≥ 1.1) and falls back toxinPath/xinValueon older versions. - A
TosiSignalis a real Angular signal (isSignal()is true) with itsset/updateredirected to write through to tosijs. - SSR: tosijs needs DOM globals to load, so server rendering means a DOM-shimmed pipeline (see tosijs#18).
Development
bun start— build, watch, and serve the demo at http://localhost:8017bun run build— one-shot build ofdist/(library) anddocs/(demo site)bun test— run the test suitebun run typecheck— compile-only type tests
