Vue 3 ref changes the DOM but reactive doesn't - vue.js

The following code works and I can see the output as intended when use ref, but when using reactive, I see no changes in the DOM. If I console.log transaction, the data is there in both cases. Once transaction as a variable changes, should the changes not be reflected on the DOM in both cases?
I'm still trying to wrap my head around Vue 3's composition API and when to use ref and reactive. My understanding was that when dealing with objects, use reactive and use ref for primitive types.
Using ref it works:
<template>
{{ transaction }}
</template>
<script setup>
import { ref } from 'vue'
let transaction = ref({})
const getPayByLinkTransaction = () => {
axios({
method: "get",
url: "pay-by-link",
params: {
merchantUuid: import.meta.env.VITE_MERCHANT_UUID,
uuid: route.params.uuid,
},
})
.then((res) => {
transaction.value = res.data
})
.catch((e) => {
console.log(e)
})
}
getPayByLinkTransaction()
</script>
Using reactive it doesn't work:
<template>
{{ transaction }}
</template>
<script setup>
import { reactive } from 'vue'
let transaction = reactive({})
const getPayByLinkTransaction = () => {
axios({
method: "get",
url: "pay-by-link",
params: {
merchantUuid: import.meta.env.VITE_MERCHANT_UUID,
uuid: route.params.uuid,
},
})
.then((res) => {
transaction = { ...res.data }
})
.catch((e) => {
console.log(e)
})
}
getPayByLinkTransaction()
</script>

Oh, when you do transaction = { ...res.data } on the reactive object, you override it, like you would with any other variable reference.
What does work is assigning to the reactive object:
Object.assign(transaction, res.data)
Internally, the object is a Proxy which uses abstract getters and setters to trigger change events and map to the associated values. The setter can handle adding new properties.
A ref() on the other hand is not a Proxy, but it does the same thing with its .value getter and setter.
From what I understand, the idea of reactive() is not to make any individual object reactive, but rather to collect all your refs in one single reactive object (somewhat similar to the props object), while ref() is used for individual variables. In your case, that would mean to declare it as:
const refs = reactive({transaction: {}})
refs.transaction = { ...res.data }
The general recommendation seems to be to pick one and stick with it, and most people seem to prefer ref(). Ultimately it comes down to if you prefer the annoyance of having to write transaction.value in your script or always writing refs.transaction everywhere.

With transaction = { ...res.data } the variable transaction gets replaced with a new Object and loses reactivity.
You can omit it by changing the data sub-property directly or by using ref() instead of reactivity()
This works:
let transaction = ref({})
transaction.data = res.data;
Check the Reactivity in Depth and this great article on Medium Ref() vs Reactive() in Vue 3 to understand the details.
Playground
const { createApp, ref, reactive } = Vue;
const App = {
setup() {
const transaction1 = ref({});
let transaction2 = reactive({ data: {} });
const res = { data: { test: 'My Test Data'} };
const replace1 = () => {
transaction1.value = res.data;
}
const replace2 = () => {
transaction2.data = res.data;
}
const replace3 = () => {
transaction2.data = {};
}
return {transaction1, transaction2, replace1, replace2, replace3 }
}
}
const app = Vue.createApp(App);
app.mount('#app');
#app { line-height: 2; }
[v-cloak] { display: none; }
<div id="app">
transaction1: {{ transaction1 }}
<button type="button" #click="replace1()">1</button>
<br/>
transaction2: {{ transaction2 }}
<button type="button" #click="replace2()">2</button>
<button type="button" #click="replace3()">3</button>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>

Since reactive transaction is an object try to use Object.assign method as follows :
Object.assign(transaction, res.data)

Related

Passing an axios response to the template in Vue 3 & Composition API

<template>
<div class="home">
<h1>BPMN Lint Analyzer</h1>
<!-- Get File from DropZone -->
<DropZone #drop.prevent="drop" #change="selectedFile"/>
<span class="file-info">File:{{dropzoneFile.name}}</span>
<button #click="sendFile" >Upload File</button>
<!-- Display Response Data (Not Working)-->
<div v-if="showResponseData">
<p>Testing: {{responseData}}</p>
</div>
</div>
</template>
<script>
import DropZone from '#/components/DropZone.vue'
import {ref} from "vue"
import axios from 'axios'
export default {
name: 'HomeView',
components: {
DropZone
},
setup(){
let dropzoneFile = ref("")
//Define Response variable and visibility toggle
var responseData=''
// var showResponseData = false
//Methods
const drop = (e) => {
dropzoneFile.value = e.dataTransfer.files[0]
}
const selectedFile = () => {
dropzoneFile.value = document.querySelector('.dropzoneFile').files[0]
}
//API Call
const sendFile = () => {
let formData = new FormData()
formData.append('file', dropzoneFile.value)
axios.post('http://localhost:3000/fileupload', formData,{
headers: {
'Content-Type':'multipart/form-data'
}
}).catch(error => {
console.log(error)
}).then(response => {
responseData = response.data
console.log(responseData);
})
// showResponseData=true
}
return{dropzoneFile, drop, selectedFile, sendFile}
}
}
</script>
I'm trying to pass the response from sendFile, which is stored in responseData back to the template to display it in a div to begin with. I'm not sure if a lifecycle hook is needed.
Current output:
I played around with toggles, I tried to convert everything to options API. Tried adding logs but I'm still struggling to understand what I'm looking for.
Unfortunately I am stuck with the Composition API in this case even if the application itself is very simple. I'm struggling to learn much from the Docs so I'm hoping to find a solution here. Thank you!
You need to make responseData reactive, so try to import ref or reactive from vue:
import {ref} from 'vue'
then create your variable as a reactive:
const responseData = ref(null)
set data to your variable:
responseData.value = response.data
in template check data:
<div v-if="responseData">
<p>Testing: {{responseData}}</p>
</div>
finally return it from setup function (if you want to use it in template):
return{dropzoneFile, drop, selectedFile, sendFile, responseData}

How to destructure object props in Vue the way like you would do in React inside a setup?

I am wondering how to destructure an object prop without having to type data.title, data.keywords, data.image etc. I've tried spreading the object directly, but inside the template it is undefined if I do that.
Would like to return directly {{ title }}, {{ textarea }} etc.
My code:
<template>
<div>
<h1>{{ title }}</h1>
</div>
</template>
<script lang="ts">
import { useSanityFetcher } from "vue-sanity";
import { defineComponent, reactive, toRefs } from "vue";
export default defineComponent({
name: "App",
setup: () => {
const articleQuery = `*[_type == "article"][0] {
title,
textarea,
}`;
const options = {
listen: true,
clientOnly: true,
};
const res = useSanityFetcher<any | object>(articleQuery, options);
const data = reactive(res.data);
return toRefs(data);
},
});
</script>
Considering that useSanityFetcher is asynchronous, and res is reactive, it's incorrect to access res.data directly in setup because this disables the reactivity. Everything should happen in computed, watch, etc callback functions.
title, etc properties need to be explicitly listed in order to map reactive object to separate refs with respective names - can probably be combined with articleQuery definition or instantly available as res.data keys
E.g.:
const dataRefs = Object.fromEntries(['title', ...].map(key => [key, ref(null)]))
const res = ...
watchEffect(() => {
if (!res.data) return;
for (const key in dataRefs)
dataRefs[key] = res.data[key];
});
return { ...dataRefs };
Destructuring the object is not the problem, see Vue SFC Playground
<script lang="ts">
//import { useSanityFetcher } from "vue-sanity";
import { defineComponent, reactive, toRefs } from "vue";
export default defineComponent({
name: "App",
setup: () => {
const res = {
data: {
title: 'Hi there'
}
}
const data = reactive(res.data);
return toRefs(data);
},
});
</script>
<template>
<div>
<h1>{{ title }}</h1>
</div>
</template>
It may simply be the space between the filter and the projection in the GROQ expression
const articleQuery = `*[_type == "article"][0]{ title, textarea }`;
See A description of the GROQ syntax
A typical GROQ query has this form:
*[ <filter> ]{ <projection> }
The Vue docs actually recommend not destructing props because of the way reactivity works but if you really want to something like this should work:
const res = useSanityFetcher<any | object(articleQuery, options);
const data = reactive(res.data);
return toRefs(data);
Don't forget to import reactive and toRefs.

Vue3 Composition API - How to load default values from Ajax?

I have read everything I can find, but there is a confusing amount of variability between approaches. I want to use the "setup" form of the Vue3 composition API, which I believe is the recommended approach for future compatibility.
I have a form with elements like this:
<form #submit.prevent="update">
<div class="grid grid-cols-1 gap-6 mt-4 sm:grid-cols-2">
<div>
<label class="text-gray-700" for="accountID">ID</label>
<input disabled id="accountID" v-model="accountID"
class="bg-slate-100 cursor-not-allowed w-full mt-2 border-gray-200 rounded-md focus:border-indigo-600 focus:ring focus:ring-opacity-40 focus:ring-indigo-500"
type="text"
/>
</div>
I want to load the current values with Ajax. If the user submits the form then I want to save the changed fields with a PATCH request.
I cannot work out how to change the form value with the result of the Ajax request and still maintain the binding.
Vue3 blocks changing the props directly (which makes sense), so the code below does not work:
<script setup lang="ts">
import { ref, onMounted, computed } from "vue";
import axios from "axios";
import { useUserStore } from "#/stores/userStore";
const userStore = useUserStore();
const props = defineProps({
accountID: String,
});
const emit = defineEmits(['update:accountID'])
const accountID = computed({
get() {
return props.accountID;
},
set (value) {
return emit('update:accountID')
},
})
onMounted(async () => {
let response = await axios.get("http://localhost:8010/accounts", { headers: { "Authorization": "Bearer " + userStore.jws } });
// This is a readonly variable and cannot be reassigned
props.accountID = response.data.ID;
});
function update() {
console.log("Form submitted")
}
</script>
How can I set the form value with the result of the Ajax request?
Instead of trying to assign props.accountID, update the accountID computed prop, which updates the corresponding v-model:accountID via the computed setter. That v-model update is then reflected back to the component through the binding:
onMounted(async () => {
let response = await axios.get(…)
// props.accountID = response.data.ID ❌ cannot update readonly prop
accountID.value = response.data.ID ✅
})
Also note that your computed setter needs to emit the new value:
const accountID = computed({
get() {
return props.accountID
},
set(value) {
// return emit('update:accountID') ❌ missing value
return emit('update:accountID', value) ✅
},
})
demo

Computed property not updating in production build

I have a computed property named wildcardItem that is working when using a development build, but when I run the production build (mix --production), the property is no longer updating.
I'm using Laravel Mix to compile the code.
mix.setPublicPath('../')
.js('js/app.js', 'dist/app.js')
.vue()
.postCss('css/app.css', 'dist/app.css', [
require('postcss-import'),
require('#tailwindcss/nesting'),
require('tailwindcss'),
require('autoprefixer'),
])
.options({
manifest: false,
});
Component Setup
const items = ref([]);
const query = ref('');
const wildcardItem = computed(_ => {
console.log('Computing wildcard...');
return {
label: query.value,
};
});
document.addEventListener('CustomEvent', function (event) {
items.value = [
...event.detail.items,
wildcardItem,
];
});
Component Template
<template>
<div>
<input v-model="query" />
<div v-for="(item, index) in items" :key="`item-${index}`">
{{ item.label }}
</div>
</div>
</template>
I also don't see my console.log when running with the production build.
Can someone please guide me on why it's not working? :)
computed() returns a ref, so you need to use .value to unwrap the ref for the value:
document.addEventListener('CustomEvent', function (event) {
items.value = [
...event.detail.items,
//wildcardItem, ❌
wildcardItem.value, ✅
];
});
demo 1
Alternatively, you could use the reactivity transform, which does not require any unwrapping (no .value needed). Instead of importing ref and computed, use $ref and $computed (no imports needed):
<script setup>
let items = $ref([])
let query = $ref('')
const wildcardItem = $computed(_ => {
console.log('Computing wildcard...')
return {
label: query,
}
})
document.addEventListener('CustomEvent', function (event) {
items = [
...event.detail.items,
wildcardItem,
]
})
</script>
demo 2
Another issue you were seeing was that items was not updated when wildcardItem changed. You would need to refactor your solution to make items a computed property based on the wildcardItem appended to items from the custom event:
<script setup>
import { ref, computed } from 'vue'
const customEventItems = ref([])
const query = ref('')
const wildcardItem = computed(_ => {})
const items = computed(() => [...customEventItems.value, wildcardItem.value])
document.addEventListener('CustomEvent', function (event) {
customEventItems.value = [...event.detail.items]
})
</script>
demo 3

How do i rewrite data to variables using pusher in vue js?

I want to ask how do I rewrite vue js variable data when I use pusher on vue js.
In this case the pusher I have will change the data every 5 minutes but here I don't rewrite the previous variable.
Usually I only use:
<template>
<div class="animated fadeIn">
<b-card>
<b-card-header>
Record User
</b-card-header>
<b-card-body>
<div>
<h3>Name : {{ name }}</h3>
<h4>Email : {{ email }}</h4>
</div>
</b-card-body>
</b-card>
</div>
</template>
<script>
import Pusher from 'pusher-js'
export default {
name: 'Index',
data() {
return {
name: 'John Doe',
email: 'jdoe#gmail.com'
}
},
created () {
this.subscribe()
},
methods: {
subscribe () {
let pusher = new Pusher(process.env.VUE_APP_PUSHER_KEY, { cluster: 'ap1', forceTLS: true })
pusher.subscribe('users')
pusher.bind('list', data => {
console.log(data);
this.name = data.name
this.email = data.email
})
},
},
}
</script>
But it hasn't changed, please help.
Thank you
The problem is that pusher will append it's own context during bind. There is a way to get around it though
bind function allows you to pass the context as the 3rd parameter. You can pass this after the handler like this:
subscribe () {
let pusher = new Pusher(process.env.VUE_APP_PUSHER_KEY, { cluster: 'ap1', forceTLS: true })
pusher.subscribe('users')
pusher.bind('list', data => {
console.log(data);
this.name = data.name
this.email = data.email
}, this) // <=== pass this as context
},
ref: https://pusher.com/docs/channels/using_channels/events#binding-with-optional-this-context
if that doesn't work, you can also use the that var, which should escape the context issue.
subscribe () {
let that = this;
let pusher = new Pusher(process.env.VUE_APP_PUSHER_KEY, { cluster: 'ap1', forceTLS: true })
pusher.subscribe('users')
pusher.bind('list', data => {
console.log(data);
that.name = data.name
that.email = data.email
})
},
You might want to try the vue-pusher library which might handle the context to be more vue-friendly.
https://www.npmjs.com/package/vue-pusher
Why does that work?
there's nothing special about that, but in javascript this is a special variable that references the context. In some cases, when dealing with callback functions, the context changes. assigning this to a new variable that, stores the context of the vue method in a variable that you can then reference it even if, in this case, Pusher bind function binds a different context.