Some gotchas
useSnapshot(state) without property access will always trigger re-render
Ref: https://github.com/pmndrs/valtio/issues/209#issuecomment-896859395
Suppose we have this state (or store).
const state = proxy({
  obj: {
    count: 0,
    text: 'hello',
  },
})
If using the snapshot with accessing count,
const snap = useSnapshot(state)
snap.obj.count
it will re-render only if count changes.
If the property access is obj,
const snap = useSnapshot(state)
snap.obj
then, it will re-render if obj changes. This includes count changes and text changes.
Now, we can subscribe to the portion of the state.
const snapObj = useSnapshot(state.obj)
snapObj
This is technically same as the previous one. It doesn't touch the property of snapObj, so it will re-render if obj changes.
In summary, if a snapshot object (nested or not) is not accessed with any properties, it assumes the entire object is accessed, so any change inside the object will trigger re-render.
Using React.memo with object props may result in unexpected behavior (v1 only)
⚠️ This behavior is fixed in v2.
The snap variable returned by useSnapshot(state) is tracked for render optimization.
If you pass the snap or some objects in snap to a component with React.memo,
it may not work as expected because React.memo can skip touching object properties.
Side note: react-tracked has a special memo exported as a workaround.
We have some options:
- Do not use React.memo.
- Do not pass objects to components with - React.memo(pass primitive values instead).
- Pass in the proxy of that element, and then - useSnapshoton that proxy.
Example of (b)
const ChildComponent = React.memo(
  ({
    title, // string or any primitive values are fine.
    description, // string or any primitive values are fine.
    // obj, // objects should be avoided.
  }) => (
    <div>
      {title} - {description}
    </div>
  ),
)
const ParentComponent = () => {
  const snap = useSnapshot(state)
  return (
    <div>
      <ChildComponent
        title={snap.obj.title}
        description={snap.obj.description}
      />
    </div>
  )
}
Example of (c)
const state = proxy({
  objects: [
    { id: 1, label: 'foo' },
    { id: 2, label: 'bar' },
  ],
})
const ObjectList = React.memo(() => {
  const stateSnap = useSnapshot(state)
  return stateSnap.objects.map((object, index) => (
    <Object key={object.id} objectProxy={state.objects[index]} />
  ))
})
const Object = React.memo(({ objectProxy }) => {
  const objectSnap = useSnapshot(objectProxy)
  return objectSnap.bar
})
When to use state and when to use snap in functional components
- snap should be used in render function, every other cases state.
- callback functions are not in the render body and therefore state must be used.
const Component = () => {
  // this is in render body
  const handleClick = () => {
    // this is NOT in render body
  }
  return <button onClick={handleClick}>button</button>
}
- deps in useEffect should be used extracting primitive values from snap. For example: const { num, string, bool } = snap.watchObj.
- changing a state value based on other state values (without involving values like props in a component), should preferably done outside react.
subscribe(state.subscribeData, async () => {
  state.results = await load(state.someData)
})
Issue with array proxy
The following use case can occur unexpected results on arr subscription:
const byId = {}
arr.forEach((item) => {
  byId[item.id] = item
})
arr.splice(0, arr.length)
arr.push(newValue())
someUpdateFunc(byId)
Object.keys(byId).forEach((key) => arr.push(byId[key]))
Issues may arise when handling the array proxy reference in the subsequent steps:
- Subscribe array proxy
- Use the proxy as snapshot
- Assign temp variable for updating
- Remove proxy from the array
- Update temp
- Push temp in the original array
Example issue case:
const a = proxy([
  {
    nested: {
      nested: {
        test: 'apple',
      },
    },
  },
])
const sa = snapshot(a) // b.
// a.
subscribe(a, () => {
  const updated = snapshot(a)
  console.log('this is updated proxy. test is Banana', a)
  console.log('however, for the snapshot of a, test is still apple', updated)
})
function handle() {
  const temp = a[0] // c.
  a.splice(0, 1) // d.
  temp.nested.nested.test = 'Banana' // e.
  a.push(temp) // f.
  console.log(Object.is(temp, a[0])) // this will be true
}
To work around this, swap d and e:
// ...
function handle() {
  const temp = a[0]
  temp.nested.nested.test = 'Banana' // Update first remove from array
  a.splice(0, 1)
  a.push(temp)
}
// ...
If the workaround is not applied and you are using react with devtools(), the redux devtools will notify a value update, but the snapshot will remain the same within the devtools' subscription.
As a result, the devtools will not display any state change.
Additionally, this issue involved not only updating devtools, but also triggering re-render.