sveltejs / svelte · Issue No. 18716
Ownership validation treats every $state proxy as some component's state. A library that creates reactive objects and hands them to app components has no way to say otherwise, so its users get warned for using the library the way it is meant to be used.
Say a collection owns its rows and exposes them through a getter.
export function createCollection() {
let id = 0;
const rows = $state([{ id: id++, heading: 'Row 1' }]);
return {
get rows() {
return rows;
},
add(heading) {
rows.push({ id: id++, heading });
}
};
}
An app renders them, and updates one in place because that is the library's documented API.
{#each collection.rows as row (row.id)}
<Row {row} />
{/each}
<!-- Row.svelte -->
<script>
let { row } = $props();
</script>
<input bind:value={row.heading} />
That warns on every keystroke. Neither Row.svelte nor the component rendering the list created the row.
Binding is not a way out. In runes mode bind:row on an each argument is a compile error. bind:row={collection.rows[i]} reads the getter and assigns into whatever array came back, so it lands only while that is the same array every read. A collection that pages or filters returns a fresh one and the write disappears with no warning at all.
That leaves a choice between telling users to ignore a warning, adding a second way to write purely to satisfy a dev check, and handing out a plain object wrapper instead of a proxy so the bail from #15759 applies. The wrapper works, but only at the top level. Pass a nested object one level further down and it is a proxy again.
Let a value say it is not component state, and bail on it in create_ownership_validator. A symbol on the object would do, or an exported helper.
import { unowned } from 'svelte';
const rows = unowned($state([{ id: 0, heading: 'Row 1' }]));
That is one more condition beside the existing value?.[STATE_SYMBOL] check.
#15678 narrowed this warning to fire "only in simple and easy-to-reason-about cases". Library-owned state is the opposite of that, and static analysis has no way to recognise it.
would make my life easier
Relay reads this issue against the repository's contribution signals: the files it is likely to touch, how the maintainers triage work this size, and what the first contribution would exercise.
The full analysis for this issue is still being assembled. Until then, the description above and the thread on GitHub are the most reliable context.