-
-
Notifications
You must be signed in to change notification settings - Fork 260
Some gotchas
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.
Object.keys(state.obj)
doesn't touch any object properties, so the same as above.
It's possible to use non-proxy rendering:
const [keys, setKeys] = useState([]);
useEffect(() => {
const callback = () => {
const next = Object.keys(state.obj);
setKeys((prev) => (prev.length === next.length && prev.every((k) => next.includes(k)) ? prev: next);
}
const unsubscribe = subscribe(state.obj, callback);
callback(); // run just once
return unsubscribe;
}, [])
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:
a. Do not use React.memo
.
b. Do not pass objects to components with React.memo
(pass primitive values instead).
c. Pass in the proxy of that element, and then useSnapshot
on that proxy.
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>
)
}
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;
});
- 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 state const
{ num, string, bool } = snap.watchObj
or (not preferred) with [snap.num, snap.string, snap.bool] directly (less stable). - 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)
})