Access data on main component from composable - VueJS 3 - vue.js

I was wondering what could be the best approach to access data on my setup function, from a composable method... If it is possible at all (and a good practice).
For example, on my main component, I would have a reactive data object with all of the variables used in that component... but not all of them will be used by my composable.
Here's an example of what I'm trying to do (It's kinda like a model composable)...
Let's say that I have a useDrivers composable, and want to use its saveDriver method, something like this:
// all shortened for readability
import useDrivers from "#composables/use-drivers";
export default {
setup() {
const { saveDriver } = useDrivers();
const data= reactive({
foo: null,
bar: null,
accident: { ... },
driver: { ... },
anotherUsedVar: null,
});
// instead of doing something like this:
saveDriver(driver, accident.id, anotherUsedVar);
// or even worse:
saveDriver(driver.name, driver.gender, driver.license, accident.id, anotherUserVar);
// Could I, somehow, access my DATA const from inside my method?
// but, again, without having to pass whatever amount of parameters
saveDriver();
}
}

If data is static, it can be a single context object:
const context = { name: driver.name, ... };
saveDriver(context);
If data is supposed to stay reactive inside saveDriver, a computed ref could be passed:
const context = computed(() => ({ name: driver.name, ... });
saveDriver(context);
Then data needs to be processed inside saveDriver with usual composition API - computed, watch, etc.

Related

Issue using Socket.io with Pinia

I'm trying to use Pinia to manage some global state in a Vue.js app I'm building, specifically I want to share a Socket.io instance between various components and views. However I'm getting a
this.socketObject.emit is not a function
error when calling functions from the Socket.io instance, when I call them from a component other than the component/view the Socket.io instance was created in. Here's some excerpts of the code.
#/views/LobbyView.vue (This is where I create the Socket.io instance and pass it to the Pinia store, I can use emit fine in this file without any errors)
import io from "socket.io-client";
import { useSocket} from "#/store/index";
...
setup() {
const socketObject = useSocket();
return { socketObject};
},
...
async mounted() {
this.socketObject = await io("http://localhost:8000");
this.socketObject.emit("createNewRoom");
}
#/store/index.js Pinia store
import { defineStore } from "pinia";
...
export const useSocket = defineStore({
id: "socket",
state: () => {
return {socketObject: Object};
},
getters: {},
actions: {},
});
#/components/lobbySettings (this is the file where I have issues using Socket.io in via my Pinia store)
import { useSocket } from "#/store/index";
...
setup() {
const socketObject = useSocket();
return { socketObject};
},
...
methods: {
startGame() {
this.socketObject.emit("updateRoom", this.roomInfo);
},
},
When the start game method is called on a button press, if I catch the error I get
this.socketObject.emit is not a function
I don't quite understand why Pinia isn't giving me access to functions from my Socket.io instance, the store seems to be working fine for other data in my app, just cant call these functions.
useSocket returns a store, not socket instance. It should be used as:
const socketStore = useSocket();
...
socketStore.socketObject.emit(...)
io(...) doesn't return a promise, it's semantically incorrect to use it with await.
The use of Object constructor is incorrect. If a value is uninitialized, it can be null:
state: () => {
return {socketObject: null};
},
The mutation of store state outside the store is a bad practice. All state modifications should be performed by actions, this way they can be easily tracked through devtools, this is one of benefits of using a store.
At this point there's no benefit from packing socketObject inside a store. Socket instance could be either used separately from a store, or socket instance could be abstracted away and made reactive with store actions, etc.

Destructure Reactive object in Vue script setup

I'm following the Vue 3 documents on how to move to using the <script setup> tags to simplify my component code.
One of the perks of using this setup is that you no longer have to use the export default boilerplate to explicitly return an object: anything declared on the top level scope will be automagically available in templates.
The issue I have is that in my app I have a very large object as my initial state, which in my normal Vue 3 app I can return and have automatically destructured, like this:
<script>
import { reactive, toRefs } from 'vue'
export default {
setup() {
const state = reactive({
foo: 1,
bar: 2,
// the rest of a very large object
})
return toRefs(state)
}
}
</script>
This saves me having to declare each and every item in the object as its own ref(), removing boilerplate.
My question is, how can I achieve the same auto-destructuring in the mode of Vue, where it only detects top level declarations? I want to be able to reference the keys of the object directly, without having to use state.foo or state.bar, but not have to explicitly declare every single one as a const in order to make it available in the
<script setup>
import { reactive, toRefs } from 'vue'
const state = reactive({
foo: 1,
bar: 2,
// the rest of a very large object
})
const { foo, bar, ? } = toRefs(state) // how do I destructure this dynamically?
</script>
You can destructure your object like you are doing and save the rest of the object keys and values with the spread operator.
<script setup>
import { reactive, toRefs } from 'vue'
const state = reactive({
foo: 1,
bar: 2,
test: 'test',
// the rest of a very large object
})
const { foo, bar, ...rest } = toRefs(state) // how do I destructure this dynamically?
</script>
Every key but foo and bar can be reached by accessing the rest variable. Like rest.test
If this isn't what you're after, I don't think what you're trying to do is possible.
See this post if my answer wasn't what you're looking for:
How to destructure into dynamically named variables in ES6?

Difference between watch and $watch

Just a simple question.
What is the difference between options and instance methods?
Based on the watch example, we can implement watcher as an option (https://v3.vuejs.org/api/options-data.html#watch) and a method of an instance (https://v3.vuejs.org/api/instance-methods.html#watch).
From my point of understanding, I can implement exactly the same feature with both methods and the only differences would be the syntax and the place of implementation.
If I am mistaken, can somebody explain to me based on example the difference between these two?
You are indeed (almost) correct with your assumption.
There is 2 major advantage of this.$watch() though.
You can start watching dynamically
the return-value of this.$watch() is an unwatch function with which you can dynamically stop the watcher during runtime
But that doesn't necessarly mean that you should always use this.$watch() over watch: {}. The opposite. You should always think about what your use case needs
Unwatch-example:
export default {
//..
created(props) {
const unwatchAge = this.$watch(() => this.user.age, (value, oldValue) => {
if (value >= 18) {
alert('You are now allowed to drive a car!');
unwatchAge(); //we don't need age watching for anything else
}
});
}
//...
}
BTW with VUE3 you might wanna look into the watch() / watchEffect() composition API methods.
watch() does the same as watch: {} and this.$watch() and also has an unwatch-method as return-value.
watchEffect() checks any value mentioned inside parameter (function) and puts a watcher on it internally.
watch() Example (composition)
import { toRef, watch} from 'vue';
export default {
//...
setup(props) {
const age = toRef(props.age);
const unwatchAge = watch(age, console.log);
// no () => age or () => age.value needed as age is a reference by using toRef and references can be handles like this
setTimeout(() => {
console.warn('unwatching age!');
unwatchAge();
}, 5000);
}
//...
}
watchEffect() Example (composition)
import { toRef, watchEffect} from 'vue';
export default {
//...
setup(props) {
const age = toRef(props.age);
watchEffect(() => {
if (age.value >= 18) {
alert('You are now allowed to drive a car!');
}
});
//vue will internally recognize that age has to be watched here. No telling it manually.
}
//...
}
The main difference from the docs it is that the instance method returns a unwatchable that you can trigger to stop watching a certain property:
const unwatchUsers = this.$watch('users', () => {});
setTimeout(unwatchUsers, 1000);
This is not possible with options API. It is extremely useful to use this unwatch returned by this.$watch when something happens in your app.
Have in mind what is the most appropriate to your use case and use it accordingly

How to `emit` event out of `setup` method in vue3?

I know I can call the emit method from the setup method, but is there any way to emit event from any other functions without passing the emit method from setup method(not the the functions in the methods option, but a useXXX function) ?
setup function takes two arguments, First one is props.
And the second one is context which exposes three component properties, attrs, slots and emit.
You can access emit from context like:
export default {
setup(props, context) {
context.emit('event');
},
};
or
export default {
setup(props, { emit }) {
emit('event');
},
};
Source
in vue3 typescript setup
<script setup lang="ts">
const emit = defineEmits()
emit('type', 'data')
<script>
20220626
<script setup lang="ts">
const emit = defineEmits(['emit_a', 'emit_b'])
emit('emit_a')
emit('emit_b', 'emit_b_data')
<script>
With Vue 3 setup syntax sugar
<script setup lang="ts">
import { defineEmits } from 'vue'
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
function yourFunction (id: number) {
emit('change', id)
}
<script>
See docs: https://v3.vuejs.org/api/sfc-script-setup.html#typescript-only-features
Here's the proper way to emit events programmatically (using javascript) in vue3:
export default defineComponent({
// See: https://vuejs.org/guide/components/events.html#declaring-emitted-events=
emits: 'myEventName', // <--- don't forget to declare custom events emitted
setup(_, { emit }) {
emit('myEventName') // <--- emit custom event programmatically whenever we want
},
})
The emits function can just as easily be passed as a param to any function not declared inside setup.
Side-note regarding other answers: we should avoid using getCurrentInstance(), which was intended for library authors needing access to internals of vue components (a.k.a. this of vue v2), when there are better alternatives. Especially when those alternatives were designed explicitly for our use case.
methods: {
minhaFuncao(){
let data = "conteudo";
this.$emit("nomeDoMEuEvento", data);
}
}
SEE MORE AT :https://github.com/Carlos-Alexandre-Leutz/emitir-eventos-filho-pra-pai-com-dados-no-vue3
export const useEmit = () => {
const vm = getCurrentInstance()
const emitFactory = (event: string) => (...args: any[]) => vm.emit(event, ...args)
return {
emit: vm.emit,
emitModel: emitFactory('update:modelValue')
}
}
const useButtonHandlers = () => {
const { emit } = useEmit()
const onClick = () => emit('click')
return {
onClick
}
}
You can use getCurrentInstance from Vue. You can check it out in the docs.
Usage is like
function useFunctionThatEmitsSomething(){
const instance = getCurrentInstance();
// do something
instance.emit('event');
}
Edit: Even though this answer solves the author's problem, as per the linked docs, this method is intended only for ADVANCED use cases, e.g authoring a plugin or library. For common use cases, like building a simple SPA, using this is TOTALLY DISCOURAGED and should be avoided at all costs, since it can lead to unreadable and unmaintenable code. If you feel the need to use this in a case like that, you're probably doing something wrong.

How to get data from vuex state into local data for manipulation

I'm having trouble understanding how to interact with my local state from my vuex state. I have an array with multiple items inside of it that is stored in vuex state. I'm trying to get that data from my vuex state into my components local state. I have no problems fetching the data with a getter and computed property but I cannot get the same data from the computed property into local state to manipulate it. My end goal is to build pagination on this component.
I can get the data using a getters and computed properties. I feel like I should be using a lifecycle hook somewhere.
Retrieving Data
App.vue:
I'm attempting to pull the data before any components load. This seems to have no effect versus having a created lifecycle hook on the component itself.
export default {
name: "App",
components: {},
data() {
return {
//
};
},
mounted() {
this.$store.dispatch("retrieveSnippets");
}
};
State:
This is a module store/modules/snippets.js
const state = {
snippets: []
}
const mutations = {
SET_SNIPPETS(state, payload) {
state.snippets = payload;
},
}
const actions = {
retrieveSnippets(context) {
const userId = firebase.auth().currentUser.uid;
db.collection("projects")
.where("person", "==", userId)
.orderBy("title", "desc")
.onSnapshot(snap => {
let tempSnippets = [];
snap.forEach(doc => {
tempSnippets.push({
id: doc.id,
title: doc.data().title,
description: doc.data().description,
code: doc.data().code,
person: doc.data().person
});
});
context.commit("SET_SNIPPETS", tempSnippets);
});
}
}
const getters = {
getCurrentSnippet(state) {
return state.snippet;
},
Inside Component
data() {
return {
visibleSnippets: [],
}
}
computed: {
stateSnippets() {
return this.$store.getters.allSnippets;
}
}
HTML:
you can see that i'm looping through the array that is returned by stateSnippets in my html because the computed property is bound. If i remove this and try to loop through my local state, the computed property doesn't work anymore.
<v-flex xs4 md4 lg4>
<v-card v-for="snippet in stateSnippets" :key="snippet.id">
<v-card-title v-on:click="snippetDetail(snippet)">{{ snippet.title }}</v-card-title>
</v-card>
</v-flex>
My goal would be to get the array that is returned from stateSnippets into the local data property of visibleSnippets. This would allow me to build pagination and manipulate this potentially very long array into something shorter.
You can get the state into your template in many ways, and all will be reactive.
Directly In Template
<div>{{$store.state.myValue}}</div>
<div v-html='$store.state.myValue'></div>
Using computed
<div>{{myValue}}</div>
computed: {
myValue() { return this.$store.state.myValue }
}
Using the Vuex mapState helper
<div>{{myValue}}</div>
computed: {
...mapState(['myValue'])
}
You can also use getters instead of accessing the state directly.
The de-facto approach is to use mapGetters and mapState, and then access the Vuex data using the local component.
Using Composition API
<div>{{myValue}}</div>
setup() {
// You can also get state directly instead of relying on instance.
const currentInstance = getCurrentInstance()
const myValue = computed(()=>{
// Access state directly or use getter
return currentInstance.proxy.$store.state.myValue
})
// If not using Vue3 <script setup>
return {
myValue
}
}
I guess you are getting how Flux/Vuex works completely wrong. Flux and its implementation in Vuex is one way flow. So your component gets data from store via mapState or mapGetters. This is one way so then you dispatch actions form within the component that in the end commit. Commits are the only way of modifying the store state. After store state has changed, your component will immediately react to its changes with latest data in the state.
Note: if you only want the first 5 elements you just need to slice the data from the store. You can do it in 2 different ways:
1 - Create a getter.
getters: {
firstFiveSnipets: state => {
return state.snipets.slice(0, 5);
}
}
2 - Create a computed property from the mapState.
computed: {
...mapState(['allSnipets']),
firstFiveSnipets() {
return this.allSnipets.slice(0, 5);
}
}