Vue.js 3 - replace/update reactive object without losing reactivity
Asked Answered
S

6

59

I need to update a reactive object with some data after fetching:

  setup(){
    const formData = reactive({})

    onMounted(() => {
      fetchData().then((data) => {
        if (data) {
          formData = data //how can i replace the whole reactive object?
        }
      })
    })
  }

formData = data will not work and also formData = { ...formdata, data }

Is there a better way to do this?

Sobriquet answered 15/1, 2021 at 7:35 Comment(0)
H
40

According to the official docs :

Since Vue's reactivity tracking works over property access, we must always keep the same reference to the reactive object. This means we can't easily "replace" a reactive object because the reactivity connection to the first reference is lost

reactive should define a state with nested fields that could be mutated like :

 setup(){
    const data= reactive({formData :null })

    onMounted(() => {
      fetchData().then((data) => {
        if (data) {
          data.formData = data 
        }
      })
    })

  }

or use ref if you just have one nested field:

  setup(){
    const formData = ref({})

    onMounted(() => {
      fetchData().then((data) => {
        if (data) {
          formData.value = data 
        }
      })
    })

  }
Holsworth answered 15/1, 2021 at 7:52 Comment(1)
BEWARE, this will replace not update the object. The answer with more votes should be used for more use cases.Huggins
M
118

Though Boussadjra Brahim's solution works its not the exact answer to the question.

In the sense that reactive data can not be reassigned with = but there is a way to reassign the reactive data. It is Object.assign.

Therefore this should work

    setup(){
        const formData = reactive({})
    
        onMounted(() => {
          fetchData().then((data) => {
            if (data) {
              Object.assign(formData, data) // equivalent to reassign 
            }
          })
        })
      }

Note:

This solution works when your reactive object is empty or always contains same keys.

However, if for example, formData has key x and data does not have key x then after Object.assign, formData will still have key x, so this is not strictly reassigning.

demo example; including watch

Manufactory answered 15/1, 2021 at 9:42 Comment(4)
So what can we do if we want the x key to be deleted as well?Postdiluvian
@Postdiluvian you can delete keys from objects normally (using delete obj.key) before or after Object.assign, the tricky part is figuring out which keys to be deleted. you can compare both object and find out what needs to be removed, but it generally indicates some flaw in logicManufactory
Perhaps structuredClone() seems to be even better for this purpose: developer.mozilla.org/en-US/docs/Web/API/structuredCloneBide
@Bide I can't see how that would work? structuredClone returns a new object you have to assign in its entirety thus losing the deep bindings. I mean just doing formData=structuredClone(data) doesn't help. If you have another technique how it can also delete properties perhaps write it up as a full answer? I'd be interested.Bentinck
H
40

According to the official docs :

Since Vue's reactivity tracking works over property access, we must always keep the same reference to the reactive object. This means we can't easily "replace" a reactive object because the reactivity connection to the first reference is lost

reactive should define a state with nested fields that could be mutated like :

 setup(){
    const data= reactive({formData :null })

    onMounted(() => {
      fetchData().then((data) => {
        if (data) {
          data.formData = data 
        }
      })
    })

  }

or use ref if you just have one nested field:

  setup(){
    const formData = ref({})

    onMounted(() => {
      fetchData().then((data) => {
        if (data) {
          formData.value = data 
        }
      })
    })

  }
Holsworth answered 15/1, 2021 at 7:52 Comment(1)
BEWARE, this will replace not update the object. The answer with more votes should be used for more use cases.Huggins
G
2

Using Object.assign may work for simple cases, but it will destroy the references in deeply nested objects, so it's not an universal solution. Plus, referential loss is very hard to debug (guess how I know this...).

The best solution I came up so far, as I published in my blog, is a function to deeply copy the fields from one object to another, while handling a few corner cases, which will save you from some headaches:

/**
 * 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));
    }
}

Usage goes like this:

const basicPerson = { // ordinary object
    name: 'Joe',
    age: 42,
};

const mary = reactive({ // reactive object
    name: 'Mary',
    age: 36,
});

deepAssign(mary, basicPerson); // mary is now basic
Germany answered 29/11, 2023 at 18:59 Comment(0)
J
1

The first answer given by Boussadjra Brahim argues that for reactive objects, you should define a state with nested fields. This increases complexity of our codes and makes it less readable. Besides, in most situations, we do not want to change the original structure of our code.

Or it proposes that we use ref instead of reactive. The same thing holds again. Sometimes we prefer to not change our code structures, because we should replace all instances of reactive objective with ref one and as you know, in this situation, we should add an extra "value" property to new "ref" variable everywhere. This makes our codes to change in several possible situations and keep tracking of all of them, might result in errors and inconsistency.

In my opinion, one good solution is using Object.keys and forEach iteration to copy each fields of new object in our reactive object fields in just one line as follows [By this solution, there is no extra change in our code]:

  setup(){
        const formData = reactive({})
    
        onMounted(() => {
          fetchData().then((data) => {
            if (data) {
             Object.keys(data).forEach(key=>formData[key]=data[key])
            }
          })
        })
      }
Juliettejulina answered 22/1 at 4:53 Comment(1)
Thank you for your interest in contributing to the Stack Overflow community. This question already has quite a few answers—including one that has been extensively validated by the community. Are you certain your approach hasn’t been given previously? If so, it would be useful to explain how your approach is different, under what circumstances your approach might be preferred, and/or why you think the previous answers aren’t sufficient. Can you kindly edit your answer to offer an explanation?Plessor
A
0

I think the method by using ref and updating by orig.value = newValue is the currently the best.

Abeyance answered 21/2, 2023 at 9:5 Comment(3)
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.Kier
What's unclear about it? It's correct and by far the most concise.Catamenia
It's unclear in the sense that none of "ref", "orig" or "newValue" appear in the question so it's not clear what this answer is even referring to.Capitoline
E
0

If you want to keep the reactivity in the target object but don't want to bind its reactivity to the source object, you can do it like shown below.

I use this pattern to get data from the store into the component but keep a local state to be able to explicitly save or discard the changes:

import { computed, reactive } from 'vue'
import { useMyStuffStore } from '@/stores/myStuffStore'

const { myStuff } = useMyStuffStore()

const form = reactive(JSON.parse(JSON.stringify(myStuff.foo)))

const hasPendingChanges = computed(() => {
  return JSON.stringify(form) !== JSON.stringify(myStuff.foo)
})

function saveChanges () {
  Object.assign(myStuff.foo, JSON.parse(JSON.stringify(form)))
}

function discardChanges () {
  Object.assign(form, JSON.parse(JSON.stringify(myStuff.foo)))
}

Within myStuffStore the myStuff object is declared as reactive.

You can now directly use the keys within form as v-model in input fields, e.g.

<label for="name">Name:</label>
<input type="text" v-model="form.name" id="name" />

Changes will be synced to the store when `saveChanges()` is being called and can be discarded by calling `discardChanges()`.
Elimination answered 7/3 at 14:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.