For a legacy project we need to use vue 2.
However we want to implement state by using #vue/composition-api for vue 2
But my only question is, how to use it with options api?
I have a proof of concept with a .js file
import { reactive } from '#vue/composition-api';
const state = reactive({
counter : 0
})
export default{ state }
For setup it is easy:
<template>
<h1>hi
<div>We still in it: {{ counter }}</div>
<button #click="increment">+</button>
</h1>
</template>
<script>
import { defineComponent, computed } from '#vue/composition-api'
export default defineComponent({
name: "TestStateHello",
setup() {
const store = require("./useState").default;
return {
counter: computed(() => store.state.counter),
increment: () => store.state.counter++,
};
},
})
</script>
But when i want to use regular options api to have access to reactive state of counter i don't seem to know how.
your help will be very grateful!
Just import it globally (outside of the returned options object):
<template>
<h1>hi
<div>We still in it: {{ counter }}</div>
<button #click="increment">+</button>
</h1>
</template>
<script>
import { defineComponent, computed } from '#vue/composition-api'
// Alternative (after fixing export): import {store} from './useState';
// You can use this in setup, too - no need to the require inside the setup()
const store = require("./useState").default;
export default defineComponent({
name: "TestStateHello",
computed: {
counter: () => store.state.counter,
},
methods: {
increment: () => store.state.counter++,
}
})
</script>
I suggest you change the export to:
import { reactive } from '#vue/composition-api';
const state = reactive({
counter : 0
})
export state; // < then import works as above
When dealing with options api you can make use of provide-inject-concept
How it works.
You can provide file of store in main.js like below
import GStore from "./useState"
app.provide('GStore',GStore)
Then in component you can inject that store
export default {
inject:["GStore"]
methods:{
//Code just to show how to access store counter
testingState(){
return this.GStore.state.counter;
}
}
}
Related
I am using this video https://www.youtube.com/watch?v=Ok6vO98RV_Q&t=108s, but I keep getting the error: "error 'counter' is assigned a value but never used no-unused-vars". What am I missing?
<template>
{{ counter }}
</template>
<script>
import { storeToRefs } from "pinia";
import useStore from "../store/useStore";
const main = useStore;
const { counter } = storeToRefs(main);
</script>
store:
import { defineStore } from "pinia";
// import { auth } from "./auth.module";
export const useStore = defineStore("main", {
state: () => ({
counter: null,
}),
});
You have to specify <script setup> for Composition API or else you have to expose the properties manually to the template by returning them.
<template>
{{ counter }}
</template>
<script setup>
import { storeToRefs } from "pinia";
import useStore from "../store/useStore";
const main = useStore();
const { counter } = storeToRefs(main);
</script>
I'm wondering how I can observe child properties from the parent component in Vue 3 using the composition api (I'm working with the experimental script setup).
<template>//Child.vue
<button
#click="count++"
v-text="'count: ' + count"
/>
</template>
<script setup>
import { ref } from 'vue'
let count = ref(1)
</script>
<template>//Parent.vue
<p>parent: {{ count }}</p> //update me with a watcher
<Child ref="childComponent" />
</template>
<script setup>
import Child from './Child.vue'
import { onMounted, ref, watch } from 'vue'
const childComponent = ref(null)
let count = ref(0)
onMounted(() => {
watch(childComponent.count.value, (newVal, oldVal) => {
console.log(newVal, oldVal);
count.value = newVal
})
})
</script>
I want to understand how I can watch changes in the child component from the parent component. My not working solution is inspired by the Vue.js 2 Solution asked here. So I don't want to emit the count.value but just watch for changes.
Thank you!
The Bindings inside of <script setup> are "closed by default" as you can see here.
However you can explicitly expose certain refs.
For that you use useContext().expose({ ref1,ref2,ref3 })
So simply add this to Child.vue:
import { useContext } from 'vue'
useContext().expose({ count })
and then change the Watcher in Parent.vue to:
watch(() => childComponent.value.count, (newVal, oldVal) => {
console.log(newVal, oldVal);
count.value = newVal
})
And it works!
I've answered the Vue 2 Solution
and it works perfectly fine with Vue 3 if you don't use script setup or explicitly expose properties.
Here is the working code.
Child.vue
<template>
<button #click="count++">Increase</button>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
return {
count: ref(0),
};
},
};
</script>
Parent.vue
<template>
<div id="app">
<Child ref="childComponent" />
</div>
</template>
<script>
import { ref, onMounted, watch } from 'vue';
import Child from './components/Child.vue';
export default {
components: {
Child,
},
setup() {
const childComponent = ref(null);
onMounted(() => {
watch(
() => childComponent.value.count,
(newVal) => {
console.log({ newVal }) // runs when count changes
}
);
});
return { childComponent };
},
};
</script>
See it live on StackBlitz
Please keep reading
In the Vue 2 Solution I have described that we should use the mounted hook in order to be able to watch child properties.
In Vue 3 however, that's no longer an issue/limitation since the watcher has additional options like flush: 'post' which ensures that the element has been rendered.
Make sure to read the Docs: Watching Template Refs
When using script setup, the public instance of the component it's not exposed and thus, the Vue 2 solutions will not work.
In order to make it work you need to explicitly expose properties:
With script setup
import { ref } from 'vue'
const a = 1
const b = ref(2)
defineExpose({
a,
b
})
With Options API
export default {
expose: ['publicData', 'publicMethod'],
data() {
return {
publicData: 'foo',
privateData: 'bar'
}
},
methods: {
publicMethod() {
/* ... */
},
privateMethod() {
/* ... */
}
}
}
Note: If you define expose in Options API then only those properties will be exposed. The rest will not be accessible from template refs or $parent chains.
I have the following code:
<!-- App.vue -->
<template>
<button #click="login">Login</button>
</template>
<script lang="ts">
import { loggedIn } from '../state'
import { defineComponent } from 'vue'
export default defineComponent({
data() {
return {
loggedIn,
}
},
methods: {
login() {
this.loggedIn = true
}
}
})
</script>
// state.ts
import { reactive, ref } from 'vue'
export const loggedIn = ref(true)
The code above has a compilation error (which shows both in VS Code and from vue-cli-service serve)
TS2322: Type 'true' is not assignable to type 'Ref<boolean>'.
> | this.loggedIn = true
I'm pretty sure that's how I'm supposed to do it, so I'm not sure why I'm getting an error. I can change the code to this and the error goes away: this.loggedIn.value = true But I'm pretty sure that's not how its supposed to work, and I get this runtime error:
Cannot create property 'value' on boolean 'false'
Why am I getting this compilation error in my original code?
Source: https://codesandbox.io/s/sad-cdn-ok40d
App.vue
<!-- App.vue -->
<template>
<div>
<button #click="login">Login</button>
<div>{{ data }}</div>
</div>
</template>
<script lang="ts">
import { loggedIn } from "./state";
export default {
name: "App",
setup() {
const data = loggedIn();
const login = () => (data.value = !data.value);
return { data, login };
},
};
</script>
State.ts
import { ref } from "vue";
export const loggedIn = () => ref(false);
Main.js
import { createApp } from "vue";
import App from "./App.vue";
createApp(App).mount("#app");
So the thing is, if we use the composition API, we should use the setup() method to setup out data and methods.
Since ref uses the .value to change the value, we don't need reactive.
Reactive is used for object values - which it will add a Proxy to watch over the key/values of the object.
In this case, we should use ref.
I am developing a single-page-application using vue-cli3 and npm.
The problem: Populating a basic integer value (stored in a vuex state) named counter which was incremented/decremented in the backend to the frontend, which displays the new value.
The increment/decrement mutations are working fine on both components (Frontend/Backend), but it seems like the mutations don't work on the same route instance: When incrementing/ decrementing the counter in backend, the value is not updated in the frontend and otherwise.
store.js:
Contains the state which needs to be synced between Backend/Frontend.
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
counter: 10
},
mutations: {
increment (state) {
state.counter++
},
decrement (state) {
state.counter--
}
}
})
index.js:
Defines the routes that the vue-router has to provide.
import Vue from 'vue'
import Router from 'vue-router'
import Frontend from '#/components/Frontend'
import Backend from '#/components/Backend'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'Frontend',
component: Frontend
},
{
path: '/backend',
name: 'Backend',
component: Backend
}
],
mode: 'history'
})
main.js:
Inits the Vue instance and provides the global store and router instances.
import Vue from 'vue'
import App from './App'
import router from './router'
import { sync } from 'vuex-router-sync'
import store from './store/store'
Vue.config.productionTip = false
sync(store, router)
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
Frontend.vue/Backend.vue:
Both (Frontend/Backend) use the same code here.
They use the state counter in order to display and modify it.
<template>
<div> Counter: {{ getCounter }}
<br>
<p>
<button #click="increment">+</button>
<button #click="decrement">-</button>
</p>
</div>
</template>
<script>
export default {
name: 'Frontend',
methods: {
increment () {
this.$store.commit('increment')
},
decrement () {
this.$store.commit('decrement')
}
},
computed: {
getCounter () {
return this.$store.state.counter
}
}
}
</script>
It would be awesome if someone sould tell me what I am missing or if I have misunderstood the concept of vuex and vue-router.
Just get the counter from the store for both components. You don't need data as store is already reactive.
<template>
<div> Counter: {{ counter }}
<br>
<p>
<button #click="increment">+</button>
<button #click="decrement">-</button>
</p>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex';
export default {
name: 'Frontend',
methods: {
...mapMutations([
'increment',
'decrement',
])
},
computed: {
...mapState({
counter: state => state.counter,
})
}
}
</script>
For reference:
mapState: https://vuex.vuejs.org/guide/state.html#the-mapstate-helper
mapMutations: https://vuex.vuejs.org/guide/mutations.html#committing-mutations-in-components
#sebikolon component properties that are defined in data () => {} are reactive, methods are not, they are called once. Instead of {{ getCounter }}, just use {{ $store.state.counter }}. OR initiate property in each component that gets the value of your state.
data: function () {
return {
counter: $store.state.counter,
}
}
My component does not update the loaded property when Store.loaded changes:
Component
import { Vue } from 'vue-property-decorator'
import Component from 'nuxt-class-component'
import { Store } from '../repositories'
#Component
export default class Layout extends Vue {
loaded = Store.loaded
}
Store
class Root {
loaded = false
}
export let Store = new Root()
export default Store
In your example Store is just plain function (class), without any reactivity (no Vue watchers attached for Store.loaded field).
Only properties inside component's data are reactive. If you want reactive single store outside of vue components (better for big frontend applications), you should use Vuex
Simple example will be:
App.vue:
<script>
import { mapGetters, mapMutations } from 'vuex';
import store from './store';
import ChildComponent from './components/ChildComponent.vue';
export default {
store,
components: { ChildComponent },
methods: {
...mapMutations(['toggleLoaded']),
},
computed: {
...mapGetters({
isLoaded: 'isLoaded',
}),
}
}
</script>
<template>
<div id="app">
Toggle loaded
<h3>Root component: </h3>
<div>The loaded flag is: {{ isLoaded }}</div>
<ChildComponent />
</div>
</template>
components/ChildComponent.vue:
<script>
import { mapGetters } from 'vuex';
export default {
computed: {
...mapGetters({
isLoaded: 'isLoaded', //accessing to same data, as root through single Vuex state
}),
}
}
</script>
<template>
<div class="hello">
<h3>Child component</h3>
<div>The loaded flag is: {{ isLoaded }}</div>
</div>
</template>
And reactive Vuex store:
store/index.js:
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const state = {
loaded: false
};
const getters = {
isLoaded: state => state.loaded,
};
const mutations = {
toggleLoaded: (state) => {
state.loaded = !state.loaded;
}
};
export default new Vuex.Store({
state,
mutations,
// actions,
getters,
strict: true
});
You can find full source of this example on GitHub.