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.