Skip to content

Suggestion: Add notify method to allow signal based reactivity #1189

@JannisKoeksel

Description

@JannisKoeksel

🚀 Feature Proposal

Add an optional notfy_ function to the ProxyState that defaults to null. So that the state in createProxyProxy looks like this:

const state: ProxyState = {
	type_: isArray ? ArchType.Array : (ArchType.Object as any),
	// Track which produce call this is associated with.
	scope_: parent ? parent.scope_ : getCurrentScope()!,
	// True for both shallow and deep changes.
	modified_: false,
	// Used during finalization.
	finalized_: false,
	// Track which properties have been assigned (true) or deleted (false).
	assigned_: {},
	// The parent draft state.
	parent_: parent,
	// The base state.
	base_: base,
	// The base proxy.
	draft_: null as any, // set below
	// The base copy with any updated values.
	copy_: null,
	// Called by the `produce` function.
	revoke_: null as any,
	isManual_: false,

	// new optional method for change notification; defaulting to null
	notify_: null
}

and then updating the object trap to wrap the set function:

 export const objectTraps: ProxyHandler<ProxyState> = {
	...
	set(state: ProxyObjectState, prop: string /* strictly not, but helps TS */, value) {
		const res = originalObjectTrapsSet(state,prop,value)
	
		if(state.notify_) state.notify_()
		return res 
	}

Motivation

The immer Proxy does not play nice with the svelte 5 rune proxy. Adding the notify_ method would allow adding reactivity to drafts.
I can't see any downsides in having the notify_ method as it has no noticeable performance impact when not used, is backwards compatible and opt in.

Can this be solved in user-land code?

I have not found a satisfying user land solution. Doesn't mean there isn't one

Example

A simple implementation that allows the default svelte 5 bind:value on inputs. Comments on svelte specific code; would work similar with other signal based systems.

const DRAFT_IMMER_PROXY = Symbol.for('og-immer-proxy');

function makeDraft<T extends object>(data: T) {
	const draft = createDraft(data);

	//update is a svelte internal signal to notify dependent values 
	const subscribe = createSubscriber((update) => {
		draft[DRAFT_STATE].notify_ = () => {
			update();
		};
	});

	const traps: ProxyHandler<Draft<T>> = {
		get(target, p, receiver) {
			if (p == DRAFT_IMMER_PROXY) {
				return draft;
			}
			
			// create svelte subscription
			subscribe();
			return Reflect.get(target, p, receiver);
		}
	};
	const proxy = new Proxy(draft, traps);

	return proxy;
}

function commit(draft: Draft<any>) {
	const immerDrat = draft[DRAFT_IMMER_PROXY];
	if (!immerDrat) return;
	let fwd;
	let bwd;
	const res = finishDraft(immerDrat, (p, u) => {
		fwd = p;
		bwd = u;
	});
	return [res, fwd, bwd];
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions