tosi, xin, and xinProxy

This documentation is mainly here for explanatory purposes. Just use tosi() as described in the parent doc.

The key to managing application state with tosijs are its observer proxies, tosi and xin. These are documented here.

The basic idea is pretty simple.

If you assign a value to a proxy, things that are bound to that proxy will be updated. E.g.

import { elements, tosi } from 'tosijs'

consy myProxy = tosi({
  myProxy: {
    foo: 'bar'
  }
})

// create an element and bind its textContent to myProxy.foo
//
const myDiv = elements.div({ bindText: myProxy.foo })

myProxy.foo = 'now it is updated'
// wait until the next animation frame is rendered
myDiv.textContent === 'now it is updated'

It's not magic. It's proxies. And the updates are surgical. The rest of the DOM isn't touched.

xinProxy() and tosi()

tosi() was formerly called boxedProxy().

After coding with xin for a while, it became apparent that a common pattern was something like this:

import myThing as _myThing from 'path/to/my-thing'
import { xin } from 'tosijs'

xin.myThing = _myThing
export const myThing = xin.myThing as typeof _myThing

Now we can use the new myThing in a pretty intuitive way, leverage autocomplete most of the time, and it's all pretty nice.

And because myThing.path.to.something is actually a XinProxy we can actually bind to it directly. So instead of typing (or mis-typing):

customElement({bindValue: 'mything.path.to.something'}))

We can type the following and even use autocomplete:

customElement({bindValue: mything.path.to.something}))

This gets you:

const { myThing } = xinProxy({ myThing: ... })

And after working with that for a while, the question became, what if we could leverage autocomplete even for non-object properties?

This gets us to:

const { myThing } = boxedProxy({ myThing: ... })

…(and also boxed).

boxed and boxedProxy deliver a proxy wrapped around an object wrapped around the original string, boolean, or number. This gets you autocomplete and strong typing in general, at the cost of slight annoyances (e.g. having to write myThing.path.to.string.valueOf() === 'some value'). That's the tradeoff. In practice it's really very nice.

xinProxy(foo) is simply declared as a function that takes an object of type T and returns a BoxedProxy.

import { xinProxy } from 'tosijs'

const { foo, bar } = boxedProxy({
  foo: 'bar',
  bar: {
    director: 'luhrmann'
  }
})

This is syntax sugar for:

import { boxed } from 'tosijs'

const stuff = {
  foo: 'bar',
  bar: {
    director: 'luhrmann',
    born: 1962
  }
}

Object.assign(boxed, stuff)

const { foo, bar } = boxed as XinProxy<typeof stuff>

So, Typescript will know that foo is a string and bar is a XinProxy<typeof stuff.bar>.

Now, boxedProxy is the same except replace XinProxy with BoxedProxy and now Typescript will know that foo is a BoxedProxy<string>, bar is a BoxedProxy<typeof stuff.bar> and bar.born is a BoxedProxy<number>.

This lets you write bindings that support autocomplete and lint. Yay!

tosiUnique()

tosiUnique() creates a reactive proxy stored under a guaranteed unique key. This is useful for component instances or any situation where you need per-instance reactive state without worrying about key collisions.

It returns a tuple of [proxy, removeFunc]:

If you pass an owner object (e.g. this in a component), the state will be automatically cleaned up when the owner is garbage collected — no need to call the remove function manually.

Typical usage in a component:

class MyComponent extends Component {
  proxy = tosiUnique({ count: 0, name: '' }, this)[0]
}

Or if you want explicit cleanup control:

const [proxy, remove] = tosiUnique({ count: 0, name: '' })
// ... later ...
remove()