I stumbled into a totally unexpected problem while refactoring my code to composition API: there doesn't seem to be any (documented) way of accessing current instance from the lifecycle hooks.
sample code:
import { defineComponent, onMounted } from 'vue';
export default defineComponent({
setup() {
onMounted(() => {
console.log(this); // <-- will be undefined
});
},
mounted() {
console.log(this); // <-- will be the component
},
}
I've spent hours trying to find a solution to this and ultimately just used the old options API to get what I want. None of examples, tutorials or documentation - that I read - use this in the hooks.
But I find it unbelievable that only undocumented getCurrentInstance would be the way to get the current instance from the hook.
So, which doc did I miss?
UPDATE
Here is the same example with a component
const { createApp, ref, onMounted } = Vue;
const MyComponent = {
setup() {
const id = ref(Math.round(Math.random() * 100000));
const count = ref(0);
const plus = () => { count.value++; }
const minus = function() { count.value--; }
onMounted(() => {
count.value = Math.round(Math.random() * 10)
});
return {id, count, plus, minus }
},
template: `id: {{id}} <button type="button" #click="minus()">-1</button>
{{count}}
<button type="button" #click="plus()">+1</button><hr/>`
}
const App = {
components: {
MyComponent
}
}
const app = createApp(App)
app.mount('#app')
<div id="app">
<my-component v-for="i in 5" />
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
What for do you need this in the component?
If you create your component with Composition API, then you can access all the properties directly, without using this.
Here is a very basic example:
const { createApp, ref, onMounted } = Vue;
const App = {
setup() {
const count = ref(0);
const up = () => { count.value++; }
const down = function() { count.value--; }
onMounted(() => {
count.value = 10
});
return {count, up, down }
}
}
const app = createApp(App)
app.mount('#app')
<div id="app">
<button type="button" #click="down()">-1</button>
{{count}}
<button type="button" #click="up()">+1</button>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
Related
I'm having troubles to update the value of a prop in a child component, the documentation says "when the parent property updates, it will flow down to the child" but that is not happening, i've made a summary of the code:
Parent's:
<script setup>
import Child from "../components/Child.vue"
var counter = 0
setInterval(() => {
counter++
console.log(counter)
}, 2000)
</script>
<template>
<Child :counter="counter"/>
</template>
Child's:
<script setup>
import { ref } from "vue"
const props = defineProps(['counter'])
const count = ref(props.counter)
</script>
<template>
<h1>{{ count }}</h1>
</template>
Anyone knows what could be wrong? I already tried to pass a ref object to the child and to put a watch function on the prop but it didn't work
The antwort from Gaetan C. is right, but not enough.
The counter data property is not reactive and changing it will not trigger updates.
var counter = 0;
should be
const counter = ref(0);
and then
counter.value++;
Here is the playground
const { createApp, ref } = Vue;
const Child = {
props: ['counter'],
template: '<h1>{{ counter }}</h1>'
}
const App = {
components: {
Child
},
setup() {
const counter = ref(0);
setInterval(() => {
counter.value++
}, 2000)
return { counter }
}
}
const app = createApp(App)
app.mount('#app')
<div id="app">
<child :counter="counter"></child>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.js"></script>
I'm currently trying using utterances, which is a github-based open source for comments.
I'm using utterances in my SSG page. Therefore, I'm using client side rendering for getting the utterances component.
Here is the code.
// blog/[id].tsx
/* eslint-disable react/no-danger */
import axios from 'axios';
import { dateFormat } from '_Utils/Helper';
import MarkdownRenderer from '_Components/MarkdownRenderer';
import Comment from '_Components/Comment';
import styles from './blog.module.scss';
const Article = ({ article }: any) => {
return (
<div className={styles.container}>
<div className={styles.header}>
<p className={styles.tag}>{article.data.attributes.tag.data.attributes.tag}</p>
<h1>{article.data.attributes.title}</h1>
<p className={styles.publishedDate}>Published at {dateFormat(article.data.attributes.publishedAt)}</p>
</div>
<main
>
<MarkdownRenderer markdown={article.data.attributes.content} />
<Comment />
</main>
</div>
);
};
export async function getStaticPaths() {
const articlePaths: any = await axios.get(`${process.env.NEXT_PUBLIC_BASE_URL}/api/articles/?populate[0]=*`);
const paths = articlePaths.data.data.map((path: any) => ({
params: { id: `${path.id}` },
}));
return { paths, fallback: false };
}
export async function getStaticProps(ctx: any) {
const { params } = ctx;
const { id } = params;
const article = await axios.get(
`${process.env.NEXT_PUBLIC_BASE_URL}/api/articles/${id}?populate[1]=tag&populate[0]=thumbnail`
);
return {
props: { article: article.data },
};
}
export default Article;
// Comment
const Comment = () => {
return (
<section
style={{ height: '350px', width: '100%' }}
ref={(elem) => {
if (!elem) {
return;
}
const scriptElem = document.createElement('script');
scriptElem.src = 'https://utteranc.es/client.js';
scriptElem.async = true;
scriptElem.setAttribute('repo', 'usernamechiho/Cobb-dev-blog');
scriptElem.setAttribute('issue-term', 'title');
scriptElem.setAttribute('theme', 'github-light');
scriptElem.setAttribute('label', 'comment');
scriptElem.crossOrigin = 'anonymous';
elem.appendChild(scriptElem);
}}
/>
);
};
export default Comment;
and the result
I was wondering why it happens and tried dynamic import with ssr: false.
However, there was nothing but the same.
Is there anything I can look for to get through this?
I'm learning Vue 3 composition API and Pinia. I'm making a todo.
When I submit a todo data through Pinia, I can submit to the DB, but it won't re-render until reload the page.
Do I need to use 'watch' to watch the state todos:[] and execute fetchTodos()?
any good solution?
here both codes, hope someone can help me. Thank you in advance.
----- VUE -----
<script setup>
import { ref, onMounted } from 'vue'
import { storeToRefs } = from 'pinia'
import { useTodoStore } from '../store/todo'
const store = useTodoStore()
const { getTodos } = storeToRefs(store)
onMounted(() => {
store.fetchTodos()
})
const todo = ref('')
const initForm = () => {
todo.value = ''
}
// submit via Pinia
const onSubmitToPinia = () => {
const payload = {
todo: todo.value,
}
store.addTodoFromPinia(payload)
initForm()
store.fetchTodo()
}
</script>
<template>
<h4>TODO</h4>
<!-- form addTodo -->
<form class="row g-4">
<div class="col-auto">
<input
class="form-control"
v-model="newName"
type="text"
placeholder="todo">
</div>
<div>
<button
class="btn btn-primary"
type="button"
#click="onSubmitToPinia(payload)">
submit through pinia</button>
</div>
</form>
<!-- render data from pinia -->
<div class="todo"
v-for="getTodo in getTodoss.todo"
:key="getTodo.id">
<b class="ms-2">{{ getTodo.todo }}</b>
</div>
</template>
---- PINIA ----
import { defineStore } from 'pinia'
import axios from "axios"
export const useAboutStore = defineStore('todo',{
state: () => {
return {
todos: []
}
},
getters: {
getTodos(state) {
return state.todos
}
},
actions: {
async fetchTodos() {
try {
const data = await axios.get('http://localhost:5000/todo')
this.todos = data.data
}
catch (error) {
alert(error)
console.log(error)
}
},
addTodoFromPinia(payload) {
const path = 'http://localhost:5000/todo'
axios.post(path, payload)
}
},
})
You don't need to use storeToRefs to accomplish what you want nor do you need to watch the state of the store.
<template>
<div class="todo"
v-for="getTodo in store.todos"
:key="getTodo.id">
<b class="ms-2">{{ getTodo.todo }}</b>
</div>
</template>
If for any reason the vue complains that the array is empty put a v-if checking if the store.todos.length is != 0.
And also fix your typos.
If the problem persists show me your new code and I help you again.
I'm new to Vue.js and I'm trying to do the following.
I have a working Vue 3 application where I'm testing the composition Api feature.
My App.vue file is simple and has the following:
<template>
<div>
<p>Counter: {{ counter }}</p>
<button #click="increaseCounter()">Increase counter</button>
</div>
</template>
<script>
import { ref } from "vue";
export default {
setup() {
const counter = ref(3);
function increaseCounter() {
counter.value++;
}
return {
counter,
increaseCounter
};
}
};
</script>
Now everything is working with this code.
What I want to do is to separate the script code into a new separate file and import in the App.vue to reuse it. I tried the following but with no luck:
<template>
<div>
<p> Msg: {{ msg }} </p>
<p>Counter: {{ counterApi.counter }}</p>
<button #click="counterApi.increaseCounter()">Increase counter</button>
</div>
</template>
<script>
import { counterApi } from "./counter-api.js";
export default {
created: () => {
counterApi.increaseCounter;
}
};
</script>
counter-api.js
import { ref } from "vue";
export default {
setup() {
const counter = ref(3);
function increaseCounter() {
counter.value++;
}
return {
counter,
increaseCounter
};
}
};
I'm getting the following error
Cannot read property 'increaseCounter' of undefined.
Thanks in advance
You did not showed what is inside counter-api.js. Based on import you have, it should be something like this:
export function CounterApi {
const counter = ref(3);
function increaseCounter() {
counter.value++;
}
return {
counter,
increaseCounter
};
}
Or if we use more ES6:
export const CounterApi = () {
const counter = ref(3);
const increaseCounter = () {
counter.value++;
}
return {
counter,
increaseCounter
};
}
You imported file, but it's not enough because you did nothing with it. Instead:
<template>
<div>
<p>Counter: {{ counter }}</p>
<button #click="increaseCounter()">Increase counter</button>
</div>
</template>
<script>
import { CounterApi } from "./counter-api";
export default {
setup() {
const { counter, increaseCounter } = CounterApi();
return {
counter,
increaseCounter
};
}
};
</script>
Or
<template>
<div>
<p>Counter: {{ someName.counter }}</p>
<button #click="someName.increaseCounter()">Increase counter</button>
</div>
</template>
<script>
import { CounterApi } from "./counter-api";
export default {
setup() {
const someName = CounterApi(); // use all under single const
return { someName };
}
};
</script>
There is plenty articles about composition API, for example: https://vueschool.io/articles/vuejs-tutorials/state-management-with-composition-api/
Also it is good idea and common convention to name this composable as useCounterApi and then const counterApi = useCounterApi() or with dectruction const { something, something } = useCounterApi()
It is all in official docs:
https://v3.vuejs.org/api/composition-api.html
A simple working example of a Vue2 dynamic component
<template>
<div>
<h1>O_o</h1>
<component :is="name"/>
<button #click="onClick">Click me !</button>
</div>
</template>
<script>
export default {
data: () => ({
isShow: false
}),
computed: {
name() {
return this.isShow ? () => import('./DynamicComponent') : '';
}
},
methods: {
onClick() {
this.isShow = true;
}
},
}
</script>
Everything works, everything is great. I started trying how it would work with the Composition API.
<template>
<div>
<h1>O_o</h1>
<component :is="state.name"/>
<button #click="onClick">Click me !</button>
</div>
</template>
<script>
import {ref, reactive, computed} from 'vue'
export default {
setup() {
const state = reactive({
name: computed(() => isShow ? import('./DynamicComponent.vue') : '')
});
const isShow = ref(false);
const onClick = () => {
isShow.value = true;
}
return {
state,
onClick
}
}
}
</script>
We launch, the component does not appear on the screen, although no errors are displayed.
You can learn more about 'defineAsyncComponent' here
https://labs.thisdot.co/blog/async-components-in-vue-3
or on the official website
https://v3.vuejs.org/api/global-api.html#defineasynccomponent
import { defineAsyncComponent, defineComponent, ref, computed } from "vue"
export default defineComponent({
setup(){
const isShow = ref(false);
const name = computed (() => isShow.value ? defineAsyncComponent(() => import("./DynamicComponent.vue")): '')
const onClick = () => {
isShow.value = true;
}
}
})
Here is how you can load dynamic components in Vue 3. Example of dynamic imports from the icons collection inside /icons folder prefixed with "icon-".
BaseIcon.vue
<script>
import { defineComponent, shallowRef } from 'vue'
export default defineComponent({
props: {
name: {
type: String,
required: true
}
},
setup(props) {
// use shallowRef to remove unnecessary optimizations
const currentIcon = shallowRef('')
import(`../icons/icon-${props.name}.vue`).then(val => {
// val is a Module has default
currentIcon.value = val.default
})
return {
currentIcon
}
}
})
</script>
<template>
<svg v-if="currentIcon" width="100%" viewBox="0 0 24 24" :aria-labelledby="name">
<component :is="currentIcon" />
</svg>
</template>
You don't need to use computed or watch. But before it loads and resolved there is nothing to render, this is why v-if used.
UPD
So if you need to change components (icons in my case) by changing props use watchEffect as a wrapper around the import function.
watchEffect(() => {
import(`../icons/icon-${props.name}.vue`).then(val => {
currentIcon.value = val.default
})
})
Don't forget to import it from vue =)
The component should be added to components option then just return it name using the computed property based on the ref property isShow :
components:{
MyComponent:defineAsyncComponent(() => import("./DynamicComponent.vue"))
},
setup(){
const isShow = ref(false);
const name = computed (() => isShow.value ? 'MyComponent': '')
const onClick = () => {
isShow.value = true;
}
}
Instead of string you should provide Component
<script setup>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
</script>
<template>
<component :is="Foo" />
<component :is="someCondition ? Foo : Bar" />
</template>