How valtio works
Ref: https://github.com/pmndrs/valtio/issues/171
This is to describe the high level abstraction of valtio.
Articles
Examples
proxy()
by examples
import { proxy, subscribe } from 'valtio'
const s1 = proxy({})
subscribe(s1, () => {
console.log('s1 is changed!')
})
s1.a = 1 // s1 is changed!
++s1.a // s1 is changed!
delete s1.a // s1 is changed!
s1.b = 2 // s1 is changed!
s1.b = 2 // (not changed)
s1.obj = {} // s1 is changed!
s1.obj.c = 3 // s1 is changed!
const s2 = s1.obj
subscribe(s2, () => {
console.log('s2 is changed!')
})
s1.obj.d = 4 // s1 is changed! and s2 is changed!
s2.d = 5 // s1 is changed! and s2 is changed!
const s3 = proxy({})
subscribe(s3, () => {
console.log('s3 is changed!')
})
s1.o = s3
s3.p = 'hello' // s1 is changed! and s3 is changed!
s2.q = s3
s3.p = 'hi' // s1 is changed! s2 is changed! and s3 is changed!
s1.x = s1
s1.a += 1 // s1 is changed!
snapshot()
by examples
import { proxy, snapshot } from 'valtio'
const p = proxy({})
const s1 = snapshot(p) // is {} but not wrapped by a proxy
const s2 = snapshot(p)
s1 === s2 // is true because p wasn't changed
p.a = 1 // mutate the proxy
const s3 = snapshot(p) // is { a: 1 }
p.a = 1 // mutation bails out and proxy is not updated
const s4 = snapshot(p)
s3 === s4 // is still true
p.a = 2 // mutate it
const s5 = snapshot(p) // is { a: 2 }
p.a = 1 // mutate it back
const s6 = snapshot(p) // creates a new snapshot
s3 !== s6 // is true (different snapshots, even though they are deep equal)
p.obj = { b: 2 } // attaching a new object, which will be wrapped by a proxy
const s7 = snapshot(p) // is { a: 1, obj: { b: 2 } }
p.a = 2 // mutating p
const s8 = snapshot(p) // is { a: 2, obj: { b: 2 } }
s7 !== s8 // is true because a is different
s7.obj === s8.obj // is true because obj is not changed
useSnapshot()
by examples
import { proxy, useSnapshot } from 'valtio'
const s1 = proxy({
counter: 0,
text: 'Good morning from valtio',
foo: {
boo: 'baz'
}
})
const MyComponent = () => {
// Using destructuring
const { text, counter } = useSnapshot(state)
// Multilevel destructiong works as well
const { text, counter, { foo }} = useSnapshot(state)
// Assigning to a snapshot obeject
const snap = useSnapshot(state)
return (() => {
<div id="main">
<h1>{ `${foo} - ${text}` }</h1>
{/* - or - */}
<h1>{ `${snap.foo.bar} = `${snap.text}}</h1>
<div>
<input
type="input"
{/* we use snapshot for reading */}
value={text}
{/* the line above equivalent to this */}
value={snap.text}
{/* we use the proxy (s1) for mutations */}
onChange={e => {
s1.text = e.target.value
}}
/>
</div>
<div>
{ counter }
<button onClick={() => s1.counter++}> + </button>
<button onClick={() => s1.counter--}> - </button>
</di>
</div>
})
}
Unorganized Notes
two kinds of proxies
valtio has two kinds of proxies, for write and read. We intentionally separate them for hooks and concurrent react.
proxy()
creates a proxy object to detect mutation, "proxy for write"
snapshot()
creates an immutable object from the proxy object
useSnapshot()
wraps the snapshot object again with another proxy (with proxy-compare
) to detect property access, "proxy for read"
snapshot creation is optimized
const state = proxy({ a: { aa: 1 }, b: { bb: 2 } })
const snap1 = snapshot(state)
console.log(snap1) // ---> { a: { aa: 1 }, b: { bb: 2 } }
++state.a.aa
const snap2 = snapshot(state)
console.log(snap2) // ---> { a: { aa: 2 }, b: { bb: 2 } }
snap1.b === snap2.b // this is `true`, it doesn't create a new snapshot because no properties are changed.
Some notes about valtio implementation in deep
valtio's proxy has only one goal: create an immutable snapshot object
some design principles:
- snapshot is created on demand
- changes are tracked only with version number
- subscription is used for notifying update (version)
- version number is hidden as implementation detail
- proxies are basically used only for version and subscription
- snapshot creation is optimized with version number
some notes about the implementation:
- proxy can be nested (created at the initialization)
- proxy can have circular structure (globalVersion to detect it)
some notes about promise handling:
- proxy can have a promise but does nothing
- when creating a snapshot, it will store the resolved value
- if it's not resolved, a special object will throw a promise/error