useSnapshot
Create a local snapshot that catches changes.
Normally, Valtio's snapshots (created via snapshot()
) are recreated on any change to a proxy, or any of its child proxies.
However useSnapshot
wraps the Valtio snapshot in an access-tracking proxy, so your component is render optimized, i.e. it will only re-render if keys that it (or its child components) specifically accessed have changed, and not on every single change to the proxy.
Usage
Read from snapshots in render, use the proxy in callbacks
Snapshots are read-only to render the JSX from their consistent view of the data.
Mutations, and also any reads in callbacks that make mutations, need to be made via the proxy, so that the callback reads & writes the latest value.
function Counter() {
const snap = useSnapshot(state)
return (
<div>
{snap.count}
<button
onClick={() => {
// also read from the state proxy in callbacks
if (state.count < 10) {
++state.count
}
}}
>
+1
</button>
</div>
)
}
Parent/Child Components
If you have a parent component use useSnapshot
, it can pass snapshots to child components and the parent & children will re-render when the snapshot changes.
For example:
const state = proxy({
books: [
{ id: 1, title: 'b1' },
{ id: 2, title: 'b2' },
],
})
function AuthorView() {
const snap = useSnapshot(state)
return (
<div>
{snap.books.map((book) => (
<Book key={book.id} book={book} />
))}
</div>
)
}
function BookView({ book }) {
// book is a snapshot
return <div>{book.title}</div>
}
If book 2's title is changed, a new snap
is created and the AuthorView
and BookView
components will re-render.
Note if BookView
is React.memo
d, the 1st BookView
will not re-render, b/c the 1st Book
snapshot will be the same instance, as only the 2nd Book
was mutated (the root Author
snapshot will also be updated since the list of books
has changed).
Child Components Making Mutations
The above approach works if BookView
is read-only; if your child component needs to make mutations, then you'll need to pass the proxy:
function AuthorView() {
const snap = useSnapshot(state)
return (
<div>
{snap.books.map((book, i) => (
<Book key={book.id} book={state.books[i]} />
))}
</div>
)
}
function BookView({ book }) {
// book is the proxy, so we can re-snap it + mutate it
const snap = useSnapshot(book)
return <div onClick={() => book.updateTitle()}>{snap.title}</div>
}
Or you can pass both the snapshot and proxy together, if you don't want to call useSnapshot
in the child component:
function AuthorView() {
const snap = useSnapshot(state)
return (
<div>
{snap.books.map((book, i) => (
<Book key={book.id} bookProxy={state.books[i]} bookSnapshot={book} />
))}
</div>
)
}
There should be no performance difference between these two approaches.
Read only what you need
Every object inside your proxy also becomes a proxy (if you don't use ref()
). So you can also use them to create
a local snapshot.
function ProfileName() {
const snap = useSnapshot(state.profile)
return <div>{snap.name}</div>
}
Gotchas
Beware of replacing the child proxy with something else, breaking your snapshot. You can see here what happens with the original proxy when you replace the child proxy.
console.log(state)
{
profile: {
name: 'valtio'
}
}
childState = state.profile
console.log(childState)
{
name: 'valtio'
}
state.profile.name = 'react'
console.log(childState)
{
name: 'react'
}
state.profile = { name: 'new name' }
console.log(childState)
{
name: 'react'
}
console.log(state)
{
profile: {
name: 'new name'
}
}
useSnapshot()
depends on the original reference of the child proxy so if you replace it with a new one, the component
that is subscribed to the old proxy won't receive new updates because it is still subscribed to the old one.
In this case, we recommend one of the approaches below. In neither example do you need to worry about re-renders because it is render-optimized.
const snap = useSnapshot(state)
return <div>{snap.profile.name}</div>
const { profile } = useSnapshot(state)
return <div>{profile.name}</div>
Dev Mode Debug Values
In dev mode, useSnapshot
uses React's useDebugValue
to output a list of fields that were accessed during rendering, i.e. which specific fields will trigger re-render when the tracking proxy changes.
There are two disclaimers to the debug value:
- Due to the way
useSnapshot
uses a proxy to recorded accesses afteruseSnapshot
has returned, the fields listed inuseDebugValue
are technically from the previous render. - Object getter and class getter calls are not included in the
useDebugValue
output, but don't worry, they are actually correctly tracked internally and correctly trigger re-renders when changed.