-
-
Save PatrickG/12586a84ba7b9a8b28f6585f542051b1 to your computer and use it in GitHub Desktop.
Svelte 5 deep reactivity
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- this makes sure we're in runes-only mode --> | |
<!-- don't enable immutable! it won't work since we're mutating the object directly --> | |
<svelte:options runes /> | |
<script> | |
import { store, increment } from './reactive.js'; | |
const deep = store.deep; | |
function push() { | |
store.items.push({ text: 'bar', done: false }); | |
} | |
function replace() { | |
store.items[0] = { text: 'baz', done: false }; | |
} | |
</script> | |
<button on:click={increment}> | |
counter: {deep.count} | |
</button> | |
<button on:click={push}>push</button> | |
<button on:click={replace}>replace</button> | |
<table> | |
<tbody> | |
<!-- see comments below as to why (item) is necessary --> | |
{#each store.items as item (item)} | |
<tr> | |
<td><input type="checkbox" bind:checked={item.done} /></td> | |
<td><input type="text" bind:value={item.text} /></td> | |
</tr> | |
{/each} | |
</tbody> | |
</table> | |
<pre>{JSON.stringify(store, null, 2)}</pre> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// props to Solid.js for this, this is the standalone version of Solid.js' createMutable API, | |
// now made to work with Svelte 5's new runes/signals reactivity system. | |
const _RAW = Symbol("store-raw"); | |
const _PROXY = Symbol("store-proxy"); | |
const _NODE = Symbol("store-node"); | |
const _HAS = Symbol("store-has"); | |
const _SELF = Symbol("store-self"); | |
const _TRACK = Symbol("store-track"); | |
// _SELF needs a unique value to go off with, so we'll just assign with this | |
// uniquely increasing counter. | |
let self_count = 0; | |
function ref(init) { | |
let value = $state(init); | |
return { | |
get value() { | |
return value; | |
}, | |
set value(next) { | |
value = next; | |
}, | |
}; | |
} | |
/** | |
* @param {any} target | |
* @param {typeof _NODE | typeof _HAS} | |
*/ | |
function getNodes(target, symbol) { | |
let nodes = target[symbol]; | |
if (!nodes) { | |
nodes = target[symbol] = Object.create(null); | |
} | |
return nodes; | |
} | |
function getNode(nodes, property, value) { | |
let state = nodes[property]; | |
if (!state) { | |
state = nodes[property] = ref(value); | |
} | |
return state; | |
} | |
function trackSelf(target) { | |
// is there a way to check if we're currently in an effect? | |
getNode(getNodes(target, _NODE), _SELF, self_count).value; | |
} | |
function isWrappable(obj) { | |
let proto; | |
return ( | |
obj != null && | |
typeof obj === "object" && | |
(obj[_PROXY] || | |
!(proto = Object.getPrototypeOf(obj)) || | |
proto === Object.prototype || | |
Array.isArray(obj)) | |
); | |
} | |
function setProperty(state, property, value, deleting) { | |
const prev = state[property]; | |
const len = state.length; | |
let has; | |
if (!deleting && prev === value) { | |
return; | |
} | |
if (value === undefined) { | |
delete state[property]; | |
if (prev !== undefined && (has = state[_HAS]) && (has = has[property])) { | |
// in Solid.js, this is set to undefined, but it only works because we can bypass the equality check. | |
// so set the values appropriately. | |
has.value = false; | |
} | |
} else { | |
state[property] = value; | |
if (prev === undefined && (has = state[_HAS]) && (has = has[property])) { | |
has.value = true; | |
} | |
} | |
const nodes = getNodes(state, _NODE); | |
let node; | |
if ((node = nodes[property])) { | |
node.value = value; | |
} | |
if (Array.isArray(state) && state.length !== len) { | |
for (let idx = state.length; idx < len; idx++) { | |
if ((node = nodes[i])) { | |
node.value = undefined; | |
} | |
} | |
if ((node = nodes.length)) { | |
node.value = state.length; | |
} | |
} | |
if ((node = nodes[_SELF])) { | |
node.value = ++self_count; | |
} | |
} | |
const Array_proto = Array.prototype; | |
const traps = { | |
get(target, property, receiver) { | |
if (property === _RAW) { | |
return target; | |
} | |
if (property === _PROXY) { | |
return receiver; | |
} | |
if (property === _TRACK) { | |
trackSelf(target); | |
return receiver; | |
} | |
const nodes = getNodes(target, _NODE); | |
const tracked = nodes[property]; | |
let value = tracked ? tracked.value : target[property]; | |
if (property === _NODE || property === _HAS || property === "__proto__") { | |
return value; | |
} | |
if (!tracked) { | |
const fn = typeof value === "function"; | |
if (fn && value === Array_proto[property]) { | |
// Svelte 5's effects are async, so we don't need to put wrap this in a batch call, | |
// so we'll just bind the Array methods to the proxy and return it as is. | |
return Array_proto[property].bind(receiver); | |
} | |
// is there a way to check if we're under an effect? | |
const desc = Object.getOwnPropertyDescriptor(target, property); | |
if ((!fn || target.hasOwnProperty(property)) && !(desc && desc.get)) { | |
value = getNode(nodes, property, value).value; | |
} | |
} | |
return isWrappable(value) ? wrap(value) : value; | |
}, | |
has(target, property) { | |
if ( | |
property === _RAW || | |
property === _PROXY || | |
property === _TRACK || | |
property === _NODE || | |
property === _HAS || | |
property === "__proto__" | |
) { | |
return true; | |
} | |
// is there a way to check if we're under an effect? | |
getNode(getNodes(target, _HAS), property).value; | |
return property in target; | |
}, | |
set(target, property, value) { | |
// Svelte 5's effects are async, so we don't need to put setProperty under a batch call | |
setProperty(target, property, unwrap(value)); | |
return true; | |
}, | |
deleteProperty(target, property) { | |
setProperty(target, property, undefined, true); | |
return true; | |
}, | |
ownKeys(target) { | |
trackSelf(target); | |
return Reflect.ownKeys(target); | |
}, | |
}; | |
export function unwrap(obj) { | |
let raw; | |
if ((raw = obj != null && obj[_RAW])) { | |
return raw; | |
} | |
return obj; | |
} | |
function wrap(obj) { | |
return (obj[_PROXY] ||= new Proxy(obj, traps)); | |
} | |
export function reactive(obj) { | |
const unwrapped = unwrap(obj); | |
const wrapped = wrap(unwrapped); | |
return wrapped; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment