Vue3 shared state - vuejs3

I want to share the state between 2 components.
One have the input form, and the other displays the result.
//useForm.js
import { reactive } from "vue"
const state = reactive({
text:""
})
export default function useForm() {
const addText = () => {
state.text = state.text + "a"
}
return {
state,
addText
};
}
//Component1.vue
<template>
<button #click="addText">Add Text</button>
</template>
<script setup>
import useForm from "src/composables/useForm";
const { addText } = useForm();
</script>
//Component2.vue
<template>
{{ state.text }}
</template>
<script setup>
import useForm from "src/composables/useForm";
const { state} = useForm();
console.log(state.text)
</script>
When I click the button in Component1.vue the state in Component2.vue remains the same. How can I make it reactive? Or What I am doing wrong?
Thanks!

Related

Vue3 composition API and pinia | submit data then reload the data

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.

Vue 3 Composition API: Update Child components props dynamically when values update from the parent component

I am trying to update a prop value when the data from the parent component gets updated and passes through the prop. The parent value always updates but does not update or re-renders in the child component when I pass it down. It passes to the prop the first time the child component is accessed but not when the data is updated in the parent component.
Below is the parent component:
<script setup>
import { inject, watchEffect, ref } from "vue";
import ChildComponent from "#/components/ChildComponent.vue"
const { state } = inject("store");
const cart = ref(state.cart);
watchEffect(() => (cart.value = state.cart));
</script>
<template>
<ChildComponent
v-for="(item, index) in cart?.items"
:key="index"
:cartItem="item"
/>
</template>
Below is the child component (only logs on the first load, never loads again):
<script setup>
import { ref, watchEffect } from "vue";
const { cartItem } = defineProps({
cartItem: !Object
});
const item = ref(cartItem);
watchEffect(() => {
console.log(item.value)
});
</script>
I have tried using Watch in many ways but it does not detect the old or the new values. It does not log any outputs
Example child component using watch:
<script setup>
import { ref, watch } from "vue";
const { cartItem } = defineProps({
cartItem: !Object
});
const item = ref(cartItem);
watch(() => item.value, (oldValue, newValue) => {
console.log(oldValue)
console.log(newValue)
});
</script>
I ended up solving the solution by using a v-if to rerender the child component.
<script setup>
import { inject, watchEffect, ref } from "vue";
import ChildComponent from "#/components/ChildComponent.vue"
const { state } = inject("store");
const cart = ref(state.cart);
const render = ref(true);
// Checks when the cart changes from the store
watchEffect(() => {
if(cart.value) {
render.value = true
}
else {
render.value = false
}
};
</script>
<template>
<div v-if="render">
<ChildComponent
v-for="(item, index) in cart?.items"
:key="index"
:cartItem="item"
/>
</div>
</template>
I had the same issue and it was frustrating, sometimes I had to do a workaround to get what I need, but try this inside the child component:
<script>
import { ref, watch } from "vue";
export default {
props: {
cartItem: {
type: !Object,
},
},
setup(props) {
const item = ref(null);
watch(props, () => {
item.value = props.cartItem;
});
return { item }
}
</script>

How to make an object passed as a prop in Vue 3 reactive?

I need to reactively change my component when a field in passed object changes.
<template>
<my-component :prop="prop" />
</template>
<script>
export default {
data() {
return {
prop: {
key: 'value',
flag: true
}
}
}
}
</script>
mycomponent.vue
<template>
<div v-if="flag">Yay, it's a flag!</div>
<div v-else>I am very sad rn</div>
</template>
<script>
export default {
props: {
prop: Object
},
setup(props) {
const prop = ref(props, 'prop')
const flag = // *
return { flag }
}
}
</script>
Don't know what to do here, prop.flag, prop.value.flag doesn't work.
I also tried something like const flag = ref(prop, 'flag') and then flag.value, or even const flag = req(prop.value, 'flag'), but no luck.
Props are accessible and reactive in components should you declare them. Since you haven't, they won't be available.
For example, this is all you need:
<template>
<div v-if="prop.flag">Yay, it's a flag!</div>
<div v-else>I am very sad rn</div>
</template>
<script>
export default {
props: {
prop: Object
}
}
</script>
Just use toRef or toRefs
<template>
<div v-if="flag">Yay, it's a flag!</div>
<div v-else>I am very sad rn</div>
</template>
<script>
import { toRefs, toRef } from 'vue';
export default {
props: {
prop: Object
},
setup(props) {
const { prop } = toRefs(props);
//alternative
const prop = toRef(props, 'prop');
const flag = // *
return { flag }
}
}
</script>

Vue 3 reuse composition API in separate file

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

Dynamic component in Vue3 Composition API

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>

Resources