npm i valtio
makes proxy-state simple
Valtio turns the object you pass it into a self-aware proxy.
import { proxy, useSnapshot } from 'valtio'
const state = proxy({ count: 0, text: 'hello' })
You can make changes to it in the same way you would to a normal js-object.
setInterval(() => {
++state.count
}, 1000)
Create a local snapshot that catches changes. Rule of thumb: read from snapshots, mutate the source. The component will only re-render when the parts of the state you access have changed, it is render-optimized.
// This will re-render on `state.count` change but not on `state.text` change
function Counter() {
const snap = useSnapshot(state)
return (
<div>
{snap.count}
<button onClick={() => ++state.count}>+1</button>
</div>
)
}
Note: useSnapshot returns a new proxy for render optimization.
Internally, useSnapshot
calls snapshot
in valtio/vanilla,
and wraps the snapshot object with another proxy to detect property access.
This feature is based on proxy-compare.
Two kinds of proxies are used for different purposes:
proxy()
fromvaltio/vanilla
is for mutation tracking or write tracking.createProxy()
fromproxy-compare
is for usage tracking or read tracking.
Use of `this` is for expert users.
Valtio tries best to handle this
behavior
but it's hard to understand without familiarity.
const state = proxy({ count: 0, inc() { ++this.count } })
state.inc() // `this` points to `state` and it works fine
const snap = useSnapshot(state)
snap.inc() // `this` points to `snap` and it doesn't work because snapshot is frozen
To avoid this pitfall, the recommended pattern is not to use this
and prefer arrow function.
const state = proxy({ count: 0, inc: () => { ++state.count } })
You can access state outside of your components and subscribe to changes.
import { subscribe } from 'valtio'
// Suscribe to all state changes
const unsubscribe = subscribe(state, () => console.log('state has changed to', state))
// Unsubscribe by calling the result
unsubscribe()
You can also subscribe to a portion of state.
const state = proxy({ obj: { foo: 'bar' }, arr: ['hello'] })
subscribe(state.obj, () => console.log('state.obj has changed to', state.obj))
state.obj.foo = 'baz'
subscribe(state.arr, () => console.log('state.arr has changed to', state.arr))
state.arr.push('world')
To subscribe to a primitive value of state, consider subscribeKey
in utils.
import { subscribeKey } from 'valtio/utils'
const state = proxy({ count: 0, text: 'hello' })
subscribeKey(state, 'count', (v) => console.log('state.count has changed to', v))
There is another util watch
which might be convenient in some cases.
import { watch } from 'valtio/utils'
const state = proxy({ count: 0 })
const stop = watch((get) => {
console.log('state has changed to', get(state))) // auto-subscribe on use
})
Valtio supports React-suspense and will throw promises that you access within a components render function. This eliminates all the async back-and-forth, you can access your data directly while the parent is responsible for fallback state and error handling.
const state = proxy({ post: fetch(url).then((res) => res.json()) })
function Post() {
const snap = useSnapshot(state)
return <div>{snap.post.title}</div>
}
function App() {
return (
<Suspense fallback={<span>waiting...</span>}>
<Post />
</Suspense>
)
}
This may be useful if you have large, nested objects with accessors that you don't want to proxy. ref
allows you to keep these objects inside the state model.
See #61 and #178 for more information.
import { proxy, ref } from 'valtio'
const state = proxy({
count: 0,
dom: ref(document.body),
})
You can subscribe a component to state without causing render, just stick the subscribe function into useEffect.
function Foo() {
const ref = useRef(state.obj)
useEffect(() => subscribe(state.obj, () => ref.current = state.obj), [state.obj])
// ...
By default, state mutations are batched before triggering re-render. Sometimes, we want to disable the batching.
function TextBox() {
const snap = useSnapshot(state, { sync: true })
return <input value={snap.text} onChange={(e) => (state.text = e.target.value)} />
}
You can use Redux DevTools Extension for plain objects and arrays.
import { devtools } from 'valtio/utils'
const state = proxy({ count: 0, text: 'hello' })
const unsub = devtools(state, 'state name')
Valtio is not tied to React, you can use it in vanilla-js.
import { proxy, subscribe, snapshot } from 'valtio/vanilla'
const state = proxy({ count: 0, text: 'hello' })
subscribe(state, () => {
console.log('state is mutated')
const obj = snapshot(state) // A snapshot is an immutable object
})
We have a convenient macro with babel-plugin-macros.
import { useProxy } from 'valtio/macro'
const Component = () => {
useProxy(state)
return (
<div>
{state.count}
<button onClick={() => ++state.count}>+1</button>
</div>
)
}
// the code above becomes the code below.
import { useSnapshot } from 'valtio'
const Component = () => {
const snap = useSnapshot(state)
return (
<div>
{snap.count}
<button onClick={() => ++state.count}>+1</button>
</div>
)
}
You can have computed values with dependency tracking. Dependency tracking in valtio conflicts with the work in useSnapshot. React users should consider using render functions (optionally useMemo) as a primary mean. Computed works well for some edge cases and for vanilla-js users.
This is to add new computed to an existing proxy state. It can add computed to different proxy state.
import { addComputed } from 'valtio/utils'
// create a base proxy
const state = proxy({
count: 1,
})
// add computed to state
addComputed(state, {
doubled: snap => snap.count * 2,
})
// create another proxy
const state2 = proxy({
text: 'hello',
})
// add computed from state to state2
addComputed(state, {
doubled: snap => snap.count * 2,
}, state2)
This is to create a proxy state with computed at the same time. It can define setters optionally.
import { proxyWithComputed } from 'valtio/utils'
const state = proxyWithComputed({
count: 1,
}, {
doubled: snap => snap.count * 2
})
// Computed values accept custom setters too:
const state2 = proxyWithComputed({
firstName: 'Alec',
lastName: 'Baldwin'
}, {
fullName: {
get: (snap) => snap.firstName + ' ' + snap.lastName,
set: (state, newValue) => { [state.firstName, state.lastName] = newValue.split(' ') },
}
})
// if you want a computed value to derive from another computed, you must declare the dependency first:
const state = proxyWithComputed({
count: 1,
}, {
doubled: snap => snap.count * 2,
quadrupled: snap => snap.doubled * 2
})
This is a utility function to create a proxy with snapshot history.
const state = proxyWithHistory({ count: 0 })
console.log(state.value) // ---> { count: 0 }
state.value.count += 1
console.log(state.value) // ---> { count: 1 }
state.undo()
console.log(state.value) // ---> { count: 0 }
state.redo()
console.log(state.value) // ---> { count: 1 }
Valtio is unopinionated about best practices. The community is working on recipes on wiki pages.