extract `onchange` callbacks from options (#15579)

* WIP

* extract onchange callbacks

* const

* tweak

* docs

* fix: unwrap args in case of spread

* fix: revert unwrap args in case of spread

---------

Co-authored-by: paoloricciuti <ricciutipaolo@gmail.com>
state-onchange
Rich Harris 6 months ago committed by GitHub
parent a33ff30e2a
commit 714c042e03
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -6,6 +6,7 @@ import * as b from '../../../../utils/builders.js';
import { regex_invalid_identifier_chars } from '../../../patterns.js'; import { regex_invalid_identifier_chars } from '../../../patterns.js';
import { get_rune } from '../../../scope.js'; import { get_rune } from '../../../scope.js';
import { should_proxy } from '../utils.js'; import { should_proxy } from '../utils.js';
import { get_onchange } from './shared/state.js';
/** /**
* @param {ClassBody} node * @param {ClassBody} node
@ -117,13 +118,16 @@ export function ClassBody(node, context) {
); );
if (field.kind === 'state' || field.kind === 'raw_state') { if (field.kind === 'state' || field.kind === 'raw_state') {
let arg = definition.value.arguments[1]; const onchange = get_onchange(
let options = arg && /** @type {Expression} **/ (context.visit(arg, child_state)); /** @type {Expression} */ (definition.value.arguments[1]),
// @ts-ignore mismatch between Context and ComponentContext. TODO look into
context
);
value = value =
field.kind === 'state' && should_proxy(init, context.state.scope) field.kind === 'state' && should_proxy(init, context.state.scope)
? b.call('$.assignable_proxy', init, options) ? b.call('$.assignable_proxy', init, onchange)
: b.call('$.state', init, options); : b.call('$.state', init, onchange);
} else { } else {
value = b.call('$.derived', field.kind === 'derived_by' ? init : b.thunk(init)); value = b.call('$.derived', field.kind === 'derived_by' ? init : b.thunk(init));
} }

@ -8,6 +8,7 @@ import * as assert from '../../../../utils/assert.js';
import { get_rune } from '../../../scope.js'; import { get_rune } from '../../../scope.js';
import { get_prop_source, is_prop_source, is_state_source, should_proxy } from '../utils.js'; import { get_prop_source, is_prop_source, is_state_source, should_proxy } from '../utils.js';
import { is_hoisted_function } from '../../utils.js'; import { is_hoisted_function } from '../../utils.js';
import { get_onchange } from './shared/state.js';
/** /**
* @param {VariableDeclaration} node * @param {VariableDeclaration} node
@ -117,26 +118,26 @@ export function VariableDeclaration(node, context) {
const args = /** @type {CallExpression} */ (init).arguments; const args = /** @type {CallExpression} */ (init).arguments;
const value = args.length > 0 ? /** @type {Expression} */ (context.visit(args[0])) : b.void0; const value = args.length > 0 ? /** @type {Expression} */ (context.visit(args[0])) : b.void0;
let options =
args.length === 2 ? /** @type {Expression} */ (context.visit(args[1])) : undefined; const onchange = get_onchange(/** @type {Expression} */ (args[1]), context);
if (rune === '$state' || rune === '$state.raw') { if (rune === '$state' || rune === '$state.raw') {
/** /**
* @param {Identifier} id * @param {Identifier} id
* @param {Expression} value * @param {Expression} value
* @param {Expression} [options] * @param {Expression} [onchange]
*/ */
const create_state_declarator = (id, value, options) => { const create_state_declarator = (id, value, onchange) => {
const binding = /** @type {Binding} */ (context.state.scope.get(id.name)); const binding = /** @type {Binding} */ (context.state.scope.get(id.name));
const proxied = rune === '$state' && should_proxy(value, context.state.scope); const proxied = rune === '$state' && should_proxy(value, context.state.scope);
const is_state = is_state_source(binding, context.state.analysis); const is_state = is_state_source(binding, context.state.analysis);
if (proxied) { if (proxied) {
return b.call(is_state ? '$.assignable_proxy' : '$.proxy', value, options); return b.call(is_state ? '$.assignable_proxy' : '$.proxy', value, onchange);
} }
if (is_state) { if (is_state) {
return b.call('$.state', value, options); return b.call('$.state', value, onchange);
} }
return value; return value;
@ -144,7 +145,7 @@ export function VariableDeclaration(node, context) {
if (declarator.id.type === 'Identifier') { if (declarator.id.type === 'Identifier') {
declarations.push( declarations.push(
b.declarator(declarator.id, create_state_declarator(declarator.id, value, options)) b.declarator(declarator.id, create_state_declarator(declarator.id, value, onchange))
); );
} else { } else {
const tmp = context.state.scope.generate('tmp'); const tmp = context.state.scope.generate('tmp');
@ -157,7 +158,7 @@ export function VariableDeclaration(node, context) {
return b.declarator( return b.declarator(
path.node, path.node,
binding?.kind === 'state' || binding?.kind === 'raw_state' binding?.kind === 'state' || binding?.kind === 'raw_state'
? create_state_declarator(binding.node, value, options) ? create_state_declarator(binding.node, value, onchange)
: value : value
); );
}) })

@ -0,0 +1,31 @@
/** @import { Expression, Property } from 'estree' */
/** @import { ComponentContext } from '../../types' */
import * as b from '../../../../../utils/builders.js';
/**
* Extract the `onchange` callback from the options passed to `$state`
* @param {Expression} options
* @param {ComponentContext} context
* @returns {Expression | undefined}
*/
export function get_onchange(options, context) {
if (!options) return;
if (options.type === 'ObjectExpression') {
const onchange = /** @type {Property | undefined} */ (
options.properties.find(
(property) =>
property.type === 'Property' &&
!property.computed &&
property.key.type === 'Identifier' &&
property.key.name === 'onchange'
)
);
if (!onchange) return;
return /** @type {Expression} */ (context.visit(onchange.value));
}
return b.member(/** @type {Expression} */ (context.visit(options)), 'onchange');
}

@ -113,15 +113,7 @@ export {
user_effect, user_effect,
user_pre_effect user_pre_effect
} from './reactivity/effects.js'; } from './reactivity/effects.js';
export { export { mutable_source, mutate, set, state, update, update_pre } from './reactivity/sources.js';
mutable_source,
mutate,
set,
state,
update,
update_pre,
get_options
} from './reactivity/sources.js';
export { export {
prop, prop,
rest_props, rest_props,

@ -28,47 +28,33 @@ function identity(fn) {
return fn; return fn;
} }
/**
* @param {ValueOptions | undefined} options
* @returns {ValueOptions | undefined}
*/
function clone_options(options) {
return options != null
? {
onchange: options.onchange
}
: undefined;
}
/** @type {ProxyMetadata | null} */ /** @type {ProxyMetadata | null} */
var parent_metadata = null; var parent_metadata = null;
/** /**
* @template T * @template T
* @param {T} value * @param {T} value
* @param {ValueOptions} [_options] * @param {() => void} [onchange]
* @param {Source<T>} [prev] dev mode only * @param {Source<T>} [prev] dev mode only
* @returns {T} * @returns {T}
*/ */
export function proxy(value, _options, prev) { export function proxy(value, onchange, prev) {
// if non-proxyable, or is already a proxy, return `value` // if non-proxyable, or is already a proxy, return `value`
if (typeof value !== 'object' || value === null) { if (typeof value !== 'object' || value === null) {
return value; return value;
} }
var options = clone_options(_options);
if (STATE_SYMBOL in value) { if (STATE_SYMBOL in value) {
// @ts-ignore // @ts-ignore
value[PROXY_ONCHANGE_SYMBOL](options?.onchange); value[PROXY_ONCHANGE_SYMBOL](onchange);
return value; return value;
} }
if (options?.onchange) { if (onchange) {
// if there's an onchange we actually store that but override the value // if there's an onchange we actually store that but override the value
// to store every other onchange that new proxies might add // to store every other onchange that new proxies might add
var onchanges = new Set([options.onchange]); var onchanges = new Set([onchange]);
options.onchange = () => { onchange = () => {
for (let onchange of onchanges) { for (let onchange of onchanges) {
onchange(); onchange();
} }
@ -116,10 +102,7 @@ export function proxy(value, _options, prev) {
if (is_proxied_array) { if (is_proxied_array) {
// We need to create the length source eagerly to ensure that // We need to create the length source eagerly to ensure that
// mutations to the array are properly synced with our proxy // mutations to the array are properly synced with our proxy
sources.set( sources.set('length', source(/** @type {any[]} */ (value).length, onchange, stack));
'length',
source(/** @type {any[]} */ (value).length, clone_options(options), stack)
);
} }
/** @type {ProxyMetadata} */ /** @type {ProxyMetadata} */
@ -165,12 +148,12 @@ export function proxy(value, _options, prev) {
var s = sources.get(prop); var s = sources.get(prop);
if (s === undefined) { if (s === undefined) {
s = with_parent(() => source(descriptor.value, clone_options(options), stack)); s = with_parent(() => source(descriptor.value, onchange, stack));
sources.set(prop, s); sources.set(prop, s);
} else { } else {
set( set(
s, s,
with_parent(() => proxy(descriptor.value, options)) with_parent(() => proxy(descriptor.value, onchange))
); );
} }
@ -184,7 +167,7 @@ export function proxy(value, _options, prev) {
if (prop in target) { if (prop in target) {
sources.set( sources.set(
prop, prop,
with_parent(() => source(UNINITIALIZED, clone_options(options), stack)) with_parent(() => source(UNINITIALIZED, onchange, stack))
); );
} }
} else { } else {
@ -201,7 +184,7 @@ export function proxy(value, _options, prev) {
// when we delete a property if the source is a proxy we remove the current onchange from // when we delete a property if the source is a proxy we remove the current onchange from
// the proxy `onchanges` so that it doesn't trigger it anymore // the proxy `onchanges` so that it doesn't trigger it anymore
if (typeof s.v === 'object' && s.v !== null && STATE_SYMBOL in s.v) { if (typeof s.v === 'object' && s.v !== null && STATE_SYMBOL in s.v) {
s.v[PROXY_ONCHANGE_SYMBOL](options?.onchange, true); s.v[PROXY_ONCHANGE_SYMBOL](onchange, true);
} }
set(s, UNINITIALIZED); set(s, UNINITIALIZED);
update_version(version); update_version(version);
@ -227,18 +210,14 @@ export function proxy(value, _options, prev) {
// we either add or remove the passed in value // we either add or remove the passed in value
// to the onchanges array or we set every source onchange // to the onchanges array or we set every source onchange
// to the passed in value (if it's undefined it will make the chain stop) // to the passed in value (if it's undefined it will make the chain stop)
if (options?.onchange != null && value && !remove) { if (onchange != null && value && !remove) {
onchanges?.add?.(value); onchanges?.add?.(value);
} else if (options?.onchange != null && value) { } else if (onchange != null && value) {
onchanges?.delete?.(value); onchanges?.delete?.(value);
} else { } else {
options = { onchange = value;
onchange: value
};
for (let [, s] of sources) { for (let [, s] of sources) {
if (s.o) { s.o = value;
s.o.onchange = value;
}
} }
} }
}; };
@ -249,7 +228,7 @@ export function proxy(value, _options, prev) {
// create a source, but only if it's an own property and not a prototype property // create a source, but only if it's an own property and not a prototype property
if (s === undefined && (!exists || get_descriptor(target, prop)?.writable)) { if (s === undefined && (!exists || get_descriptor(target, prop)?.writable)) {
let opt = clone_options(options); let opt = onchange;
s = with_parent(() => s = with_parent(() =>
source(proxy(exists ? target[prop] : UNINITIALIZED, opt), opt, stack) source(proxy(exists ? target[prop] : UNINITIALIZED, opt), opt, stack)
); );
@ -281,7 +260,7 @@ export function proxy(value, _options, prev) {
if ( if (
is_proxied_array && is_proxied_array &&
options?.onchange != null && onchange != null &&
array_methods.includes(/** @type {string} */ (prop)) array_methods.includes(/** @type {string} */ (prop))
) { ) {
return batch_onchange(v); return batch_onchange(v);
@ -330,7 +309,7 @@ export function proxy(value, _options, prev) {
(active_effect !== null && (!has || get_descriptor(target, prop)?.writable)) (active_effect !== null && (!has || get_descriptor(target, prop)?.writable))
) { ) {
if (s === undefined) { if (s === undefined) {
let opt = clone_options(options); let opt = onchange;
s = with_parent(() => source(has ? proxy(target[prop], opt) : UNINITIALIZED, opt, stack)); s = with_parent(() => source(has ? proxy(target[prop], opt) : UNINITIALIZED, opt, stack));
sources.set(prop, s); sources.set(prop, s);
} }
@ -362,14 +341,14 @@ export function proxy(value, _options, prev) {
other_s.v !== null && other_s.v !== null &&
STATE_SYMBOL in other_s.v STATE_SYMBOL in other_s.v
) { ) {
other_s.v[PROXY_ONCHANGE_SYMBOL](options?.onchange, true); other_s.v[PROXY_ONCHANGE_SYMBOL](onchange, true);
} }
set(other_s, UNINITIALIZED); set(other_s, UNINITIALIZED);
} else if (i in target) { } else if (i in target) {
// If the item exists in the original, we need to create a uninitialized source, // If the item exists in the original, we need to create a uninitialized source,
// else a later read of the property would result in a source being created with // else a later read of the property would result in a source being created with
// the value of the original item at that index. // the value of the original item at that index.
other_s = with_parent(() => source(UNINITIALIZED, clone_options(options), stack)); other_s = with_parent(() => source(UNINITIALIZED, onchange, stack));
sources.set(i + '', other_s); sources.set(i + '', other_s);
} }
} }
@ -381,7 +360,7 @@ export function proxy(value, _options, prev) {
// object property before writing to that property. // object property before writing to that property.
if (s === undefined) { if (s === undefined) {
if (!has || get_descriptor(target, prop)?.writable) { if (!has || get_descriptor(target, prop)?.writable) {
const opt = clone_options(options); const opt = onchange;
s = with_parent(() => source(undefined, opt, stack)); s = with_parent(() => source(undefined, opt, stack));
set( set(
s, s,
@ -394,11 +373,11 @@ export function proxy(value, _options, prev) {
// when we set a property if the source is a proxy we remove the current onchange from // when we set a property if the source is a proxy we remove the current onchange from
// the proxy `onchanges` so that it doesn't trigger it anymore // the proxy `onchanges` so that it doesn't trigger it anymore
if (typeof s.v === 'object' && s.v !== null && STATE_SYMBOL in s.v) { if (typeof s.v === 'object' && s.v !== null && STATE_SYMBOL in s.v) {
s.v[PROXY_ONCHANGE_SYMBOL](options?.onchange, true); s.v[PROXY_ONCHANGE_SYMBOL](onchange, true);
} }
set( set(
s, s,
with_parent(() => proxy(value, clone_options(options))) with_parent(() => proxy(value, onchange))
); );
} }
})(); })();
@ -464,11 +443,11 @@ export function proxy(value, _options, prev) {
/** /**
* @template T * @template T
* @param {T} value * @param {T} value
* @param {ValueOptions} [options] * @param {() => void} [onchange]
* @returns {Source<T>} * @returns {Source<T>}
*/ */
export function assignable_proxy(value, options) { export function assignable_proxy(value, onchange) {
return state(proxy(value, options), options); return state(proxy(value, onchange), onchange);
} }
/** /**

@ -77,7 +77,7 @@ export function batch_onchange(fn) {
/** /**
* @template V * @template V
* @param {V} v * @param {V} v
* @param {ValueOptions} [o] * @param {() => void} [o]
* @param {Error | null} [stack] * @param {Error | null} [stack]
* @returns {Source<V>} * @returns {Source<V>}
*/ */
@ -102,18 +102,10 @@ export function source(v, o, stack) {
return signal; return signal;
} }
/**
* @param {Source} source
* @returns {ValueOptions | undefined}
*/
export function get_options(source) {
return source.o;
}
/** /**
* @template V * @template V
* @param {V} v * @param {V} v
* @param {ValueOptions} [o] * @param {() => void} [o]
* @param {Error | null} [stack] * @param {Error | null} [stack]
*/ */
export function state(v, o, stack) { export function state(v, o, stack) {
@ -196,11 +188,11 @@ export function internal_set(source, value) {
if (!source.equals(value)) { if (!source.equals(value)) {
var old_value = source.v; var old_value = source.v;
if (typeof old_value === 'object' && old_value != null && source.o?.onchange) { if (typeof old_value === 'object' && old_value != null && source.o) {
// @ts-ignore // @ts-ignore
const remove = old_value[PROXY_ONCHANGE_SYMBOL]; const remove = old_value[PROXY_ONCHANGE_SYMBOL];
if (remove && typeof remove === 'function') { if (remove && typeof remove === 'function') {
remove(source.o?.onchange, true); remove(source.o, true);
} }
} }
@ -257,7 +249,7 @@ export function internal_set(source, value) {
inspect_effects.clear(); inspect_effects.clear();
} }
var onchange = source.o?.onchange; var onchange = source.o;
if (onchange) { if (onchange) {
if (onchange_batch) { if (onchange_batch) {
onchange_batch.add(onchange); onchange_batch.add(onchange);

@ -20,8 +20,8 @@ export interface Value<V = unknown> extends Signal {
rv: number; rv: number;
/** The latest value for this signal */ /** The latest value for this signal */
v: V; v: V;
/** Options for the source */ /** onchange callback */
o?: ValueOptions; o?: () => void;
/** Dev only */ /** Dev only */
created?: Error | null; created?: Error | null;
updated?: Error | null; updated?: Error | null;

Loading…
Cancel
Save