claud.io
claud.io

Reputation: 1963

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

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?

Upvotes: 71

Views: 89503

Answers (8)

JFonseca
JFonseca

Reputation: 51

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.

Upvotes: 0

LMK
LMK

Reputation: 1561

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.

Upvotes: -1

Fred
Fred

Reputation: 1818

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

Upvotes: 3

Mohsen Arab
Mohsen Arab

Reputation: 503

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

Upvotes: 1

rodrigocfd
rodrigocfd

Reputation: 8078

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

Upvotes: 3

chenqy9
chenqy9

Reputation: 25

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

Upvotes: 0

Ashwin Bande
Ashwin Bande

Reputation: 3073

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

Upvotes: 125

Boussadjra Brahim
Boussadjra Brahim

Reputation: 1

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

  }

Upvotes: 45

Related Questions