svelte icon indicating copy to clipboard operation
svelte copied to clipboard

Supported Method for Detecting Proxies

Open SBrazzCode opened this issue 10 months ago • 9 comments

Describe the problem

A common concern with svelte 5's introduction of rune proxies is the inability to detect proxies i.e issue-13562. A common issue when migrating code is sudden errors erupting from libraries/apis that don't accept proxies. Adding to that issue, there is no supported way to method to detect if an object is a reactive proxy, so user's have to use try/catch or other homegrown methods to detect if an object is a proxy.

Describe the proposed solution

Can we have a supported method for detecting proxies

Ideally, the proxy check is something integrated in Svelte's current proxy logic (linked above). Alternatively, a more generic javascript proxy detection method could work.

Importance

would make my life easier

SBrazzCode avatar Feb 20 '25 16:02 SBrazzCode

I don't think this is really that necessary; if you're concerned about whether an object is a $state proxy, you can just use $state.snapshot to unproxy the object.

Ocean-OS avatar Feb 20 '25 17:02 Ocean-OS

Interesting thing: The Svelte framework will always refer to the value the proxy is for instead of the proxy itself:

<script>
	let name = $state('John')
</script>

<pre>Is Proxy?  {name instanceof Proxy}</pre>
<pre>Is string?  {typeof name === 'string'}</pre>

And:

<script>
	let name = $state('John')

	const isProxy = $derived(name instanceof Proxy)
	const isString = $derived(typeof name === 'string')
</script>

<pre>Is Proxy?  {isProxy}</pre>
<pre>Is string?  {isString}</pre>

Tells you that name is not a proxy, and instead is a string. So how does one go about passing the actual proxy? Honestly curious.

webJose avatar Feb 20 '25 18:02 webJose

name instanceof Proxy won't work because Proxy has no prototype to compare with.

If some code cannot accept proxies, as said @Ocean-OS, just do $state.snapshot - it will unproxify the object or return the value it isn't Svelte's proxy.

7nik avatar Feb 20 '25 19:02 7nik

The Svelte framework will always refer to the value the proxy is for instead of the proxy itself

That seems like a bit of a misunderstanding. Svelte will not create proxies for primitive values (strings, numbers, booleans) at all. You need to set the state to a plain object or array for a proxy to be created. This proxy then can be passed around.

brunnerh avatar Feb 20 '25 19:02 brunnerh

I see. So I guess we learn something new everyday.

  • So Proxy has no prototype.
  • Primitive values don't create proxies.

Thanks for the lessons, @7nik and @brunnerh .

webJose avatar Feb 20 '25 19:02 webJose

  • So Proxy has no prototype.
  • Primitive values don't create proxies.

Yeah, the new Proxy syntax is kind of a misnomer, since proxies aren't class instances. I think of Proxy as a sort of intrinsic that tells the JS engine to override certain operations on the provided object. Additionally some object-related functions don't work with Proxies, such as structuredClone.

Ocean-OS avatar Feb 20 '25 20:02 Ocean-OS

Sorry to be that person who bumps an old issue, but I ran into issues today where TypeScript has been allowing structuredClone to accept objects with Svelte props, which seems to have been working fine in the browser, but now I'm trying to add Vitest and it breaks on every case.

For example, consider this Svelte component, where defaultState is a K/V object:

const { defaultState } = $props()
const initialState = structuredClone(defaultState)

Svelte makes the defaultState prop into a $state(), which seems to work with structuredClone fine in the browser (not sure why this is, I thought it would have been broken here too but has been working find since we upgraded to Svelte 5), but Vitest breaks with:

DataCloneError: Failed to execute 'structuredClone' on 'Window': #<Object> could not be cloned

As per other peoples suggestions, I'm slowly going through and adding $state.snapshot into the various locations to get things working, but I need to find these manually. It would be helpful if Svelte could do something to help out in this case.

Frustratingly, structuredClone is typed as accepting any, which obviously isn't the case, so even if Svelte typed props as Reactive<type> (or similar), TypeScript wouldn't pick this up.

Do you think it is possible for the Svelte compiler to pick up when passing props to methods imported from files that aren't .svelte or .svelte.ts? I can't think of a case where passing a $state/$derived to JS functions that can't parse them properly... e.g.

import state from './state.svelte.ts'
import store from './store.ts'
import tooltip from 'bootstrap'

const { defaultState } = $props()
const initialState = structuredClone(defaultState)

state.set(defaultState) // This is fine, state is imported from .svelte.ts file
store.set(defaultState) // Not ok, because store.ts doesn't know how to handle $state/$dervied and $state.snapshot won't be available inside it

tooltip.set("Hello") // This is fine, because the string is not $state/$derived
tooltip.set(defaultState.title) // Not ok, because defaultState is deeply reactive and so title is a $state

Would be great if that was possible and output a message, something along the lines of:

Detected passing of $state/$derived outside of Svelte context. This is usually a mistake and a call is $state.snapshot() is usually required.

And if not built into the Svelte compiler, perhaps an Svelte EsLint rule can be added. ~If that is a better place, I will go and create a ticket in the appropriate repo.~ I went ahead and created a ticket there: https://github.com/sveltejs/eslint-plugin-svelte/issues/1419

Alternatively, as per this issues original request, if we could detect if an object is a $state/$derived proxy, without using other Svelte runes, we could add some safeguards. e.g.

function doSomething(props) {
  if (props._isSvelteProxy) {
    throw new("doSomething cannot accept Svelte proxies")
  }

  // snip
}

KieranP avatar Nov 10 '25 02:11 KieranP

Svelte makes the defaultState prop into a $state()

Note, proxy is created only in two cases: (1) using $state() and (2) assigning to the proxied object or its field:

let obj = $state({}); // (1)
obj = {}; // (2);
obj.foo = {}; // (2);

So, defaultState is proxified somewhere earlier.

tooltip.set(defaultState.title) // Not ok, because defaultState is deeply reactive and so title is a $state

Usually, the name title supposes it is a string that cannot be proxied. But if it is an object...

Do you think it is possible for the Svelte compiler to pick up when passing props to methods imported from files that aren't .svelte or .svelte.ts?

No. It is impossible to statically analyze all ways of data flow (thus an ESLint rule cannot be created, or it will be very limited) and creating a runtime check also doesn't make much sense - most of JS code is tolerant to proxies, so in 98% of cases it will be an annoying useless warning. Plus, cloning data isn't free.

detect if an object is a $state/$derived proxy

As I mentioned above, $derived doesn't create a proxy, it can only bypass an existing one.

detect if an object is a $state

function isStateProxy(data) {
    if (data === null || typeof data !== "object") return false;
    if (Object.getPrototypeOf(data) !== Object.prototype) return false;
    const proxy = $state(data);
    return data === proxy;
}

Though the object/class instance still may contain a $state somewhere in the nested fields.

7nik avatar Nov 10 '25 09:11 7nik

@7nik Thank you for those clarifications. Perhaps the issue is with vitest/vitest-browser-svelte then, maybe converting the props I pass into render into a $state object. As mentioned in my last post, things have been working fine in the browser, which means defaultProps passed into the Svelte component in the browser are making it to structuredClone unaltered, but somehow defaultProps passed into vitests render are getting converted into $state before hitting structuredClone... :-/ I'll have to do some more digging tomorrow.

Quick Edit: Looks like vite-browser-svelte is culprit - https://github.com/vitest-community/vitest-browser-svelte/blob/dc33f6b8e854ea77c1c5eb9526847f8b0860083c/src/core/modern.svelte.js#L26 - Filed a bug report here: https://github.com/vitest-community/vitest-browser-svelte/issues/19

It is impossible to statically analyze all ways of data flow

With EsLint alone, perhaps not, but I imagine a type-powered check might have been possible if Svelte wrapped $state in a class like State<type> or Proxy<type>. Then type checking could have looked for all structuredClone usage and make sure the value passed in isn't a State/Proxy type. But as it currently is, type checks can't tell because the proxy svelte uses is practically invisible :-(

KieranP avatar Nov 11 '25 05:11 KieranP