1. API
  2. useSnapshot

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.memod, 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:

  1. Due to the way useSnapshot uses a proxy to recorded accesses after useSnapshot has returned, the fields listed in useDebugValue are technically from the previous render.
  2. 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.

Codesandbox demo