Reputation: 1963
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
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
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
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
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
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
Reputation: 25
I think the method by using ref
and updating by orig.value = newValue
is the currently the best.
Upvotes: 0
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.
Upvotes: 125
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