-
-
Notifications
You must be signed in to change notification settings - Fork 264
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(vanilla): unstable_buildProxyFunction instead of unstable_getHandler #528
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
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 3dd8dbf:
|
Size Change: +642 B (+1%) Total Size: 49.3 kB
ℹ️ View Unchanged
|
src/vanilla.ts
Outdated
const customObjectIs = customizeObjectIs(Object.is) | ||
const customCanProxy = customizeCanProxy(canProxy) | ||
const customCreateSnapshot = customizeCreateSnapshot(createSnapshot) | ||
const customCreateProxy = customizeCreateProxy(createProxy) |
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.
Is the reason to pass the default functions as arguments into the custom functions? Think this would be cleaner:
objectIs = options?.objectIs ?? Object.is
canProxy = options?.canProxy ?? canProxy
// ...
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.
My imaginary use case is something like:
const customizeCanProxy = (origCanProxy) => {
const canProxy = (x) => {
if (...) return true/false
return origCanProxy(x)
}
return canProxy
}
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.
Similar to the comment I left below, but I think most developers will either use all-or-none of the default behavior, instead of a mix and match. So exporting and exposing defaults would be better than passing defaults as arguments into a higher order function that the developer returns.
src/vanilla.ts
Outdated
const VERSION = __DEV__ ? Symbol('VERSION') : Symbol() | ||
const LISTENERS = __DEV__ ? Symbol('LISTENERS') : Symbol() | ||
const SNAPSHOT = __DEV__ ? Symbol('SNAPSHOT') : Symbol() | ||
const HANDLER = __DEV__ ? Symbol('HANDLER') : Symbol() | ||
const PROMISE_RESULT = __DEV__ ? Symbol('PROMISE_RESULT') : Symbol() | ||
const PROMISE_ERROR = __DEV__ ? Symbol('PROMISE_ERROR') : Symbol() |
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.
Is it possible to export these Symbols? Easier to define custom behavior when you have access all internals, instead of only having access to default methods.
For example, checking if an object has been proxied by valtio value[LISTENERS]
.
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.
For now, I still want to hide them. I might consider combining into one symbol later (then it can ease customizing.) I first tried in this PR, but it made things complicated.
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.
Here’s an example of using one symbol only: https://github.com/immerjs/immer/blob/e28b53f2d12696954b1f0125494498e34d77a3df/src/core/proxy.ts#L55
It uses one symbol to access an object which in valtio’s case can contain the listeners and version.
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.
Yep. I wish to combine SNAPSHOT
too. Removing symbols completely and using WeakMaps might be possible, but maybe performance-wise a little worth. Anyway, let's tackle it later. I would like to know what use cases this don't cover (and then the solution is either expose more or fork.)
proxyCache.set(initialObject, proxyObject) | ||
Reflect.ownKeys(initialObject).forEach((key) => { | ||
const desc = Object.getOwnPropertyDescriptor( | ||
initialObject, | ||
key | ||
) as PropertyDescriptor | ||
if (desc.get || desc.set) { | ||
Object.defineProperty(baseObject, key, desc) | ||
} else { | ||
nextValue = value | ||
proxyObject[key as keyof T] = initialObject[key as keyof T] | ||
} | ||
Reflect.set(target, prop, nextValue, receiver) | ||
notifyUpdate(['set', [prop], value, prevValue]) | ||
return true | ||
}, | ||
}) |
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.
Could be simplified by copying of keys in Reflect.ownKeys(initialObject)
into the default createProxy
method, which also allows developer to decide which keys from initialObject to keep.
const proxyObject = customCreateProxy(initialObject, handler);
proxyCache.set(initialObject, proxyObject);
return proxyObject;
const createProxy = <T extends object>(
initialObject: T,
handler: ProxyHandler<T>
): readonly [T, T] => {
const baseObject = Array.isArray(initialObject)
? []
: Object.create(Object.getPrototypeOf(initialObject))
const proxyObject = new Proxy(baseObject, handler)
Reflect.ownKeys(initialObject).forEach((key) => {
const desc = Object.getOwnPropertyDescriptor(
initialObject,
key
) as PropertyDescriptor
if (desc.get || desc.set) {
Object.defineProperty(baseObject, key, desc)
} else {
proxyObject[key as keyof T] = initialObject[key as keyof T]
}
})
return proxyObject
}
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 not sure if I understand your comment.
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 suggesting to move the for loop which copies keys from the initial object into the proxy object into the default createProxy function.
So developers can also fully customize which keys to copy if they decide not to use initialObject.
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.
Ah, I probably see your point.
I have considered that, and for this time, I decided not to add it after the consideration.
If we got use cases require such capability, we consider it in a follow up PR.
(after all, it's the balance between customization and forking.)
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.
Hm, maybe I can do that in this PR. Let me try.
src/vanilla.ts
Outdated
customizeObjectIs: (fn: typeof Object.is) => typeof Object.is = identity, | ||
customizeCanProxy: (fn: typeof canProxy) => typeof canProxy = identity, | ||
customizeCreateSnapshot: ( | ||
fn: typeof createSnapshot | ||
) => typeof createSnapshot = identity, | ||
customizeCreateProxy: ( | ||
fn: typeof createProxy | ||
) => typeof createProxy = identity | ||
) => { |
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.
Maybe change this to argument object to be less unwieldy:
const buildProxyFunction = ({
objectIs = Object.is,
canProxy = canProxy,
createSnapshot = createSnapshot,
createProxy = createProxy, // default methods
} : {
objectIs: typeof Object.is,
canProxy: typeof canProxy,
createSnapshot: typeof createSnapshot,
createProxy: typeof createProxy, // types
}) => {
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.
Then, you can't reuse the existing functions. One point of this style is to avoid exporting other functions (and symbols).
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 exporting the internals would be better, since to reuse existing functions you already need to know how they work.
Also since custom proxy is marked as unstable, developers could also understand that applies to exported internals too.
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.
Do you mean to export many unstable functions? Anyway, if this is just a preference in styles rather than capability, I'd go with this for this time.
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.
Yes. Yeah it’s a preference in style only.
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.
For more context, here's my fork of valtio that I made to work with immer:
https://gist.github.com/BrianHung/ec6c0ce729acfa0361334c128eeef7ef
Noticeable change is that by forking valtio, I can write back to the initial object and access it as well. I found keeping two separate objects, the valtio proxy object and the initial / target object, easier to work with especially when it comes to nesting.
This PR should allow such use cases too. customizeCreateProxy = (origCreateProxy) => {
const createProxy = (initialObject, handler) => {
return [initialObject, new Proxy(initialObject, handler)]
}
return createProxy
} I didn't try it though. |
9bc48a3 is to expose more functions. I'm not sure if it's worth the bundle size increase. |
Hmm, let me try this. |
Hmm, this probably doesn't solve your use case... |
My overall use case and how it differs from valtio currently is:
I can also try an having an implementation as well, by end of this week. |
PROMISE_RESULT = __DEV__ ? Symbol('PROMISE_RESULT') : Symbol(), | ||
PROMISE_ERROR = __DEV__ ? Symbol('PROMISE_ERROR') : Symbol(), |
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.
Should these be moved to shared state
and be next to VERSION
, LISTENERS
, SNAPSHOT
.
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.
Is it also possible to pass VERSION
, LISTENERS
, SNAPSHOT
into buildProxyFunction
so that a custom createSnapshot
and custom proxyFunction
could access these methods, but they're not directly exported?
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.
PROMISE_* symbols are not shared state. You can implement createSnapshot and proxyFunction without them, even just with properties. (Eventually, I will revisit this in the future, because React might be providing a new function for this. So, things will change. These are very internal.)
to pass
VERSION
,LISTENERS
,SNAPSHOT
intobuildProxyFunction
These are shared state and you would never want to change. (If you want, you still can.)
Passing them to the function doesn't allow you to read them, right? Unless we go back to customizeFoo
style, which is very complicated.
The trick is you would need to call buildProxyFunction
function twice.
Honestly, I wish to have a better solution but didn't come up with ideas (that fulfill my requirements.)
Pass your own
Pass your own
Run
I might be misunderstanding something in my approach. If you have some ideas, please feel free to share code snippets. |
Oh my confusion was over this since I didn't realize you could access |
Please check out some examples in the PR description, if you haven't. |
I agree it's confusing, but no other ideas at this moment. |
close #522
This is a breaking change on unstable features.
Some use cases
Use deepEqual instead of Object.is
Attach
get
trap to proxy handlerAvoid some objects to proxy instead of using
ref
for all