Wednesday, November 29, 2023

Setting/resetting Vue reactive objects

Vue 3 has many idiosyncrasies, among them the overlapping ref and reactive constructs. One of the main differences is that reactive content cannot be replaced. One StackOverflow answer proposes using Object.assign, but it will replace all nested references, losing them all.

I ended up writing my own version of a recursive function to replace each value inside an object, so I won’t need to deal with the muddy details ever again:

/**
 * Recursively copies each field from src to dest, avoiding the loss of
 * reactivity. Used to copy values from an ordinary object to a reactive object.
 */
export function deepAssign<T extends object>(destObj: T, srcObj: T): void {
	const dest = destObj;
	const src = toRaw(srcObj);
	if (src instanceof Date) {
		throw new Error('[deepAssign] Dates must be copied manually.');
	} else if (Array.isArray(src)) {
		for (let i = 0; i < src.length; ++i) {
			if (src[i] === null) {
				(dest as any)[i] = null;
			} else if (src[i] instanceof Date) {
				(dest as any)[i] = new Date(src[i].getTime());
			} else if (Array.isArray(src[i])
					|| typeof src[i] === 'object') {
				deepAssign((dest as any)[i], src[i]);
			} else {
				(dest as any)[i] = toRaw(src[i]);
			}
		}
	} else if (typeof src === 'object') {
		for (const k in src) {
			if (src[k] === null) {
				(dest as any)[k] = null;
			} else if (src[k] instanceof Date) {
				(dest[k] as any) = new Date((src[k] as any).getTime());
			} else if (Array.isArray(src[k])
					|| typeof src[k] === 'object') {
				deepAssign(dest[k] as any, src[k] as any);
			} else {
				(dest[k] as any) = toRaw(src[k]);
			}
		}
	} else {
		throw new Error('[deepAssign] Unknown type: ' + (typeof src));
	}
}

Another problem with reactive is that, to avoid two variables pointing to the same point, we must deep clone an object when creating the object. I also wrote a function to deal with that:

/**
 * Deeply clones an object, eliminating common references. Used to create a
 * reactive object by copying from an ordinary object.
 */
export function deepClone<T>(origObj: T): T {
	const obj = toRaw(origObj);
	if (obj === undefined
			|| obj === null
			|| typeof obj === 'string'
			|| typeof obj === 'number'
			|| typeof obj === 'boolean') {
		return obj;
	} else if (Array.isArray(obj)) {
		return obj.reduce((acum, item) => [...acum, deepClone(item)], []);
	} else if (obj instanceof Date) {
		return new Date(obj.getTime()) as unknown as T;
	} else if (typeof obj === 'object') {
		return Object.entries(obj).reduce(
			(acum, [key, val]) => ({...acum, [key]: deepClone(val)}), {}) as T;
	} else {
		throw new Error('[deepClone] Tipo desconhecido: ' + (typeof obj));
	}
}

I remember from my MobX days some people saying that working with reactive variables can be tricky. Now I can clearly see why.

No comments: