-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add api for returning a valtio proxyState from a jotai atom derived from another atom value #5
Conversation
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit 500401d:
|
Thanks for opening up a PR. API design questions:
Overall, one of my questions is if this falls into the "impure" category of Jotai mental model. |
It wouldn't be too much work to convert the code to (1). 😅
It might be possible, not sure. But I wouldn't be in favor of reverse engineering Valtio around atomEffect. The two are not similar. withProxyEffect must return a useable mutable proxy from first read.
The utility does allow Valtio-like imperative mutations similar to When That is... get(countAtom) // 0
$count.value++
get(countAtom) // 1
I should note that (i.e. the following use case should not be supported) const countAtom = atom(0)
const proxyCountAtom = withProxyEffect(countAtom, { sync: true })
function SomeComponent() {
const $count = useAtomValue(proxyCountAtom)
$count.value++
const count = useAtomValue(countAtom)
expect(count).toBe(1) // it will be 0 because count is updated on next microtask
...
} |
I think I misunderstood something. Proxies are required for mutation. So, I think it's different from
It feels to me that
This should be prohibited anyway in render. It should follow Jotai's principle. |
😮 Oh I like those choices. I wasn't too fond of withProxyEffect, it was just a placeholder.
Yes, we should lock it to
Agreed, but const countProxy = mutableAtom(0);
const readOnlyAtom = atom(get => {
const $count = get(countProxy)
$count.value++ // infinite loop ⚠️
}); |
I wish it too. For now, I think it should be clearly documented. |
Requested changes complete.
|
d099fd9
to
abbcd70
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would generally avoid leaking general get
and set
to avoid misusing in our code.
Instead, I'd pass around store
and proxyRef
(unless combined) objects, and setValue
function.
Functionality-wise, it should be the same, so basically just a suggestion for coding preference.
src/mutableAtom/index.ts
Outdated
function ensureProxyState(get: Getter, setCb: SetCb) { | ||
const proxyRef = get(proxyRefAtom) | ||
const store = get(storeAtom) | ||
if (!store.isMounted) createProxyState(get, setCb) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems to me isMounted
is somewhat misleading.
I would simply do:
if (!proxyRef.current) {
proxyRef.current = ...
}
Now, it may require combining proxyRefAtom
and storeAtom
, which I think simplifies the entire code, but I'm not sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can add both conditions.
For reasons I don't understand, when I set proxyRef.current = null
on unmount, then after resubscribing and recreating the proxy on remount, mutating the proxy does not trigger subscribe callback.
Sequence
- unmount:
a. proxyRef.current = null
b. unsubscribe() - remount:
a. proxyRef.currrent = proxyFn({ value })
b. subscribe() - mutate proxy:
a. proxyState.value++
b. // subscribe does not fire
Solution: don't do proxyRef.current = null
. I cannot understand why this is.
What should happen if proxyState were to be read / written to after the atom has unmounted?
|
I refactored storeAtom.onMount and merged it with proxyRef. Does this resolve your concern?
I think it is better to centralize this logic, but let me know if you prefer I break this up. |
BTW, reading/writing atoms without mounting is totally valid operations. import { createStore, atom } from 'jotai';
const store = createStore();
const countAtom = atom(0);
store.set(countAtom, 1);
console.log(store.get(countAtom)); This is true, even after mounting and unmounting. |
Hm, actually this is interesting. |
it('should allow updating even when component has not mounted', async () => {
const store = createStore()
const countAtom = atom({ value: 0 })
const mutableCountAtom = makeMutableAtom(countAtom)
await act(async () => {
const countProxy = store.get(mutableCountAtom)
countProxy.value++
})
expect(store.get(countAtom).value).toBe(1) // true 😮
})``` |
Great suggestions!
Done.
Done. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Getting better, but I think we can do more.
src/mutableAtom/index.ts
Outdated
void | ||
>( | ||
(get, { setSelf }) => ({ | ||
proxyState: null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can create a new proxy state here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm struggling to get proxy({ value: get(valueAtom) }) into the store.
This causes the store to recalculate on value change, but then it would need to subscribe / unsubscribe every time value changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, so, it's not here.
I don't prefer getValue: () => get(valueAtom).value,
in that sense. 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I'm not sure what you mean.
When the mutable atom is first read it creates a proxy and saves it to the store. On next read, the existing proxy is used.
Proxy is needed on first read. This can happen before the first atom mount event. So the only way I can think to synchronously create the proxy once on first read is to check the static store for its existence.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See: dmaskasky#1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I get interested in the implementation, I will work on a PR to your PR. You can then accept/reject/partially take it.
src/mutableAtom/index.ts
Outdated
void | ||
>( | ||
(get, { setSelf }) => ({ | ||
proxyState: null, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, so, it's not here.
I don't prefer getValue: () => get(valueAtom).value,
in that sense. 🤔
Here you go: dmaskasky#1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No longer used.
4a3fed9
to
3533652
Compare
I mean the goal is to drop minimalistic-assert completely. Please also resolve conflicts. |
I chose minimalistic-assert because another package we are depending on already uses it. Plus it is a devDependency so it won't increase the bundle size. I'm happy to write the logic for this too as it is simply function assert(value: boolean, message?: string) {
if (!value) {
throw new Error(message ?? 'Assertion failed')
}
}
@testing-library has a strange behavior where waitFor can run an expect that passes twice. This causes expect.assertions to fail. |
cc8bb18
to
980f7bd
Compare
Hmm, curious how it fails.
Yeah, that's much easier for me to read. Let's do that unless we find a solution to the above issue. |
It runs in a setTimeout loop, but an extra run can occur when expect is successful. The internet told me to use assert instead.
Done. |
The diff looks weird. For example, it shouldn't show changes in |
ec1053d
to
dfb5d88
Compare
…ived from another atom value
dfb5d88
to
500401d
Compare
Hopefully this is resolved now. I was in rebase hell for the past hour. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
mutableAtom
mutableAtom
wraps a value in a self-aware Valtio proxy. You can make changes to it in the same way you would to a normal js-object.API Signature
Parameters
proxyFn
for custom proxy functions.Example
Count value is stored under the
value
property.Options
Options include
proxyFn
which can beproxy
or a custom function.Caution on Mutating Proxies
Be careful not to mutate the proxy directly in the atom's read function or during render in React components. Doing so might trigger an infinite render loop.