useProxy() hook without the macro #620
Replies: 6 comments 25 replies
-
Separation of proxy and snapshot is "the reason" I started developing valtio. Having that said, what you tried is proposed several times. For example, see #38 and #238. It works. The only concern is mutating a state multiple times. const state = proxy({ count: 0 });
const Component = () => {
const writableSnapshot = useProxy(state);
const handleClick = () => {
++writableSnapshot;
++writableSnapshot;
// we expect state.count becomes `2`, but actually it's `1`
}
...
}; If we know this behavior, it's actually fine. But, this is the reason why such function isn't provided in valtio repo. My suggestion for you is to develop a third-party library. Looking forward to it. |
Beta Was this translation helpful? Give feedback.
-
https://github.com/pmndrs/valtio/releases/tag/v1.9.0 is released which includes new Someone asked this:
Short answerPlease try it. Just avoid mixing Long answer
As being a simple wrapper, not many things go wrong, and we highly recommend trying out the new hook. If you understand the implementation correctly, there's no risk using it in production. Unless people try the new hook, we don't know if there are unknown pitfalls. So, it's encouraged from the library maintainer perspective. At the same time, keeping using Known limitationsHere are some known limitations of
For 4, here's an example. const state = { nested: { count: 1 } }
const Component = () => {
const combined = useProxy(state)
const { nested } = combined // this is a snapshot
const handleClick = () => {
++nested.count // causes a runtime error
}
...
} |
Beta Was this translation helpful? Give feedback.
-
@dai-shi you say:
But my understanding is that MobX requires wrapping every React component that uses state with an To me, this is a non-trivial difference between Valtio and MobX. Personally, I actively avoid libraries that use HOCs because HOCs collide with the disadvantage's of React's rendering model and make devtools hideous and unpleasant to use. In a Solid.js world perhaps MobX does not have this disadvantage but in React the difference between Valtio and MobX is not trivial. |
Beta Was this translation helpful? Give feedback.
-
Now to speak on the biggest "gotcha" in my opinion to Valtio that everyone I know who uses Valtio has run into, it deals with the work OP has done. Valtio's insistence that useSnapshot and the proxy are separated is extremely confusing. The docs don't comment on whether the following is ok to do (and its very common code!): // counterAtom.ts
export const counterAtom = proxy({ count: 1 });
// MyComponent.tsx
import { counterAtom } from './';
const MyComponent () => {
const counterAtomSnapshot = useSnapshot(counterAtom):
return
<>
<Text>{counterAtomSnapshot.count}</Text>
<Button onPress=(() => counterAtom.count++) />
</>
} Firstly, it is not obvious to both absolute beginners and advanced users whether using both the snapshot and the atom in the same component is even ok. Is it a footgun? Has the dev done something wrong or implemented an anti-pattern? They don't know, and the docs don't clarify. Second, if it weren't for Typescript this would be so cumbersome as to be unmanageable. Even with Typescript when working heavily with Valtio I find myself accidentally writing to the snapshot constantly and getting red squigglies from Typescript or runtime errors from React Native. It sounds like this useProxy is an imperfect solution because it comes with performance implications and it doesn't proxy nested elements. However I did want to touch on that the docs are currently lacking a best practice for dealing with both the atom and the snapshot at the same time and any API that simplifies this pain point (while preserving performance) would make Valtio a premiere choice for React state management. I'm not interested in MobX for the reasons I stated above and it's reliance on weird HOCs and ES6 classes which I view as a fundamentally broken and half finished API. |
Beta Was this translation helpful? Give feedback.
-
First I wanted to work on an eslint plugin to quickly tackle with the hook limitations, but I ended up with a new useProxy() implementation which I think mitigates all the currently known problems:
For the new version, I combined the very first demo (I fixed its issues) in this thread about a referentially stable deep meta proxy, and the idea of passing along the closures a stable ref of isRendering which flips back-and-forth in render/non render. I replaced the previous simple useProxy() with this new one, and my apps work fine. It passes the render optimization test, as well as the double assignments. It has the same usage as the previous one. This deep switch proxy only traps get, otherwise it falls back to the primary proxy, so it allows setting in render. @dai-shi, this might be the ultimate solution? (NB., I ran into an issue of an unexpectedly renewing snapshot reference, but it is unrelated to this, as I reproduced that with useSnapshot as well. I'll open a bug ticket about that.) // Copied from https://github.com/pmndrs/valtio/blob/main/src/vanilla.ts#L64
const isObject = (x: unknown): x is object => typeof x === 'object' && x !== null
const canProxy = (x: unknown) =>
isObject(x) &&
// !refSet.has(x) &&
(Array.isArray(x) || !(Symbol.iterator in x)) &&
!(x instanceof WeakMap) &&
!(x instanceof WeakSet) &&
!(x instanceof Error) &&
!(x instanceof Number) &&
!(x instanceof Date) &&
!(x instanceof String) &&
!(x instanceof RegExp) &&
!(x instanceof ArrayBuffer)
function createSwitchProxy<T extends object>(
proxy: T,
snap,
plainSnap,
cache,
isRendering
): T {
return new Proxy(proxy, {
get: (target, prop) => {
if (!isRendering.current) {
return proxy[prop]
} else if (!canProxy(plainSnap[prop])) {
return snap[prop]
} else {
if (!cache.has(snap[prop])) {
cache.set(
snap[prop],
createSwitchProxy(
proxy[prop],
snap[prop],
plainSnap[prop],
new WeakMap(),
isRendering
)
)
}
return cache.get(snap[prop])
}
}
})
}
/**
* Takes a Valtio proxy and returns a new proxy which you can use in both react
* render and in callbacks. Referentially stable. For the best ergonomics, you
* can export a custom hook from your store, so you don't have to figure out a
* separate name for the hook reference. E.g.:
*
* export const store = proxyWithComputed(initialState, {})
* export const useStore = () => useProxy(store)
* // in the component file:
* function Cmp() {
* const store = useStore()
* }
*
* @param proxy Valtio proxy
* @returns A new proxy which you can use in the render as well as in callbacks.
*/
export const useProxy = <T extends object>(proxy: T) => {
const snap = useSnapshot(proxy)
const isRendering = useRef()
isRendering.current = true
useLayoutEffect(() => {
isRendering.current = false
})
return useMemo(() => {
return createSwitchProxy(
proxy,
snap,
snapshot(proxy),
new WeakMap(),
isRendering
)
}, [snap, proxy, isRendering])
} |
Beta Was this translation helpful? Give feedback.
-
What are your thoughts, should |
Beta Was this translation helpful? Give feedback.
-
Hey guys,
Ever since I started using valtio, I wondered why there's this separation between the proxy object and the snapshot in a react component. For a while I thought it's a deliberate API decision for some kind of reading/writing separation pedantry, and I wasn't aware that there's actually a transpiler macro which simplifies the usage to my desired shape.
Now I learned that given that we have this macro, it should've been a technical limitation rather than by choice. And indeed, having to deal with the snapshot reference is pretty annoying. I have to find out a name, now we have this
somethingSnap
naming convention which is pretty lengthy, and it's still just a convention; then sometimes I mix up the proxy/snapshot references, so it's a source of error as well...I started thinking of what the technical limitation can be here, and why don't we have a runtime
useProxy
hook which returns a kind of "meta proxy" which simply gives back the snapshot on reads, and the real proxy on writes.I quickly came up with an implementation which worked on the basic cases, and then I started inserting it in my apps. It turned out that my meta proxy returns new references each time the component renders, and we need a stable reference in order to make it work with useEffect etc. But it was a fixable issue, so after adding some cache mechanism, this problem has been eliminated. I replaced useSnapshot to this all across my apps, and after a few corrections, it works just fine for me.
Do I miss something here? Or should I open a PR? Obviously there might be better intergrations, because I just copied some parts from valtio source files.
Here is the code:
Beta Was this translation helpful? Give feedback.
All reactions