72

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?

8 Answers 8

129

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

Sign up to request clarification or add additional context in comments.

5 Comments

So what can we do if we want the x key to be deleted as well?
@Mocha_ 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 logic
Perhaps structuredClone() seems to be even better for this purpose: developer.mozilla.org/en-US/docs/Web/API/structuredClone
@Strinder 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.
use ref() rather than reactive() as per answers waaaaay down the page :)
45

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 
        }
      })
    })

  }

1 Comment

BEWARE, this will replace not update the object. The answer with more votes should be used for more use cases.
3

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

Comments

3

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()`.

Comments

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])
            }
          })
        })
      }

1 Comment

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?
1

For anyone that hits this again in the future, this is my simple solution without re-assigns and reactive concerns. The usual case is setting a class instance in a reactive object and let the UI update based on class internal properties.

Spoiler: do not use constructor, use a setup method and trigger it on onMounted life cycle.

Let's consider the class:

class MyClass {
   data!: SomeType

   constructor (data: SomeType) {
     this.data = data
   }

   public someMethod () {
      // ...method logic
   }
}

On a component script we intuitively do the following

const classInstance = reactive(new MyClass(data))

This won't make classInstance.data reactive because its initial value is undefined until the constructor completes.

Instead, change the class implementation to:

class MyClass {
   data!: null | SomeType = null

   setup (data: SomeType) {
     this.data = data
   }

   public someMethod () {
      if (!this.data) return ...

      // ...method logic
   }
}

Back at the component file, use setup on onMounted life cycle hook.

const classInstance = reactive(new MyClass())

onMounted(() => classInstance.setup(data)

There is the obvious downside that every class method needs to check for null values on all initial properties but, imo, its wayyyyyy better that 'hacking' the reactive behaviour in Vue.

Comments

0

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

4 Comments

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.
What's unclear about it? It's correct and by far the most concise.
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.
I have added a bit more detail in my answer with links to the official docs, but this is the correct solution imho.
-1

Rather than using reactive(someObj), use ref(someObj). This will allow you to replace the object entirely, whilst also being fully reactive.

According to the official docs at https://vuejs.org/api/reactivity-core.html:

The ref object is mutable - i.e. you can assign new values to .value. It is also reactive - i.e. any read operations to .value are tracked, and write operations will trigger associated effects. If an object is assigned as a ref's value, the object is made deeply reactive with reactive(). This also means if the object contains nested refs, they will be deeply unwrapped. To avoid the deep conversion, use shallowRef() instead.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.