Vue 3 Composition API reuse in multiple components - vue.js

I have these files
App.vue, Header.vue, search.js and Search.vue
App.vue is normal and just adding different views
Header.vue has an input box
<input type="text" v-model="searchPin" #keyup="searchResults" />
<div>{{searchPin}}</div>
and script:
import useSearch from "#/compositions/search";
export default {
name: "Header",
setup() {
const { searchPin, searchResults } = useSearch();
return {
searchPin,
searchResults
};
}
};
search.js has the reusable code
import { ref } from "vue";
export default function useSearch() {
const searchPin = ref("");
function searchResults() {
return searchPin.value;
}
return {
searchPin,
searchResults
};
}
Now, this is working well.. once you add something on the input box, it is showing in the div below.
The thing I have not understood is how to use this code to a third component like Search.vue.
I have this, but its not working.
<template>
<div>
<h1 class="mt-3">Search</h1>
<div>{{ searchPin }}</div>
</div>
</template>
<script>
import useSearch from "#/compositions/search";
export default {
name: "Search",
setup() {
const { searchPin, searchResults } = useSearch();
return {
searchPin,
searchResults
};
}
};
</script>
What am I missing? Thanks.

The fix for this is very simple
instead of
import { ref } from "vue";
export default function useSearch() {
const searchPin = ref("");
function searchResults() {
return searchPin.value;
}
return {
searchPin,
searchResults
};
}
use
import { ref } from "vue";
const searchPin = ref("");
export default function useSearch() {
function searchResults() {
return searchPin.value;
}
return {
searchPin,
searchResults
};
}
The problem is that the searchPin is scoped to the function, so every time you call the function, it gets a new ref. This is a desirable effect in some cases, but in your case, you'll need to take it out.
Here is an example that uses both, hope it clears it up.
const {
defineComponent,
createApp,
ref
} = Vue
const searchPin = ref("");
function useSearch() {
const searchPinLoc = ref("");
function searchResults() {
return searchPin.value + "|" + searchPinLoc.value;
}
return {
searchPin,
searchPinLoc,
searchResults
};
}
const HeaderComponent = defineComponent({
template: document.getElementById("Header").innerHTML,
setup() {
return useSearch();
},
})
const SearchComponent = defineComponent({
template: document.getElementById("Search").innerHTML,
setup() {
return useSearch();
}
})
createApp({
el: '#app',
components: {
HeaderComponent, SearchComponent
},
setup() {}
}).mount('#app')
<script src="https://unpkg.com/vue#3.0.0-rc.9/dist/vue.global.js"></script>
<div id="app">
<header-component></header-component>
<search-component></search-component>
</div>
<template id="Header">
searchPin : <input type="text" v-model="searchPin" #keyup="searchResults" />
searchPinLoc : <input type="text" v-model="searchPinLoc" #keyup="searchResults" />
<div>both: {{searchResults()}}</div>
</template>
<template id="Search">
<div>
<h1 class="mt-3">Search</h1>
<div>both: {{searchResults()}}</div>
</div>
</template>

Adding flavor to #Daniel 's answer.
This is exactly what I'm struggling with regarding to best practices ATM and came to some conclusions:
Pulling the Ref outside of the composition fn would fix your problem but if you think about it, it's like sharing a single instance of a data property used in multiple places. You should be very careful with this, since ref is mutable for whoever pulls it, and will easily break unidirectional data flow.
For e.g. sharing a single Ref instance between a parent component and a child components can be compared to passing it down from parent's data to child's props, and as I assume we all know we should avoid mutating props directly
So classical answer for your question would be, move it to Vuex state and read it from there.
But if you have a small application, don't want a state manager, or simply want to take full advantage of the composition API, then my suggestion would be to at least do something of this pattern
import { ref, computed } from "vue";
const _searchPin = ref(""); // Mutable persistant prop
const searchPin = computed(() => _searchPin.value); // Readonly computed prop to expose
export default function useSearch() {
function searchResults() {
return searchPin.value;
}
return {
searchPin,
searchResults
};
}
Not more than ONE component should mutate the persistent Ref while others could only listen to the computed one.
If you find that more than one component needs access to change the ref, then that's probably a sign you should find another way to implement this (Vuex, props and events, etc...)
As I said, I am still trying to make sense of this myself and am not sure this is a good enough pattern either, but it's definitely better then simply exposing the instance.
Another option for code arrangement would be to encapsulate in 2 different access hooks
import { ref, readonly } from "vue";
const searchPin = ref(""); // Mutable persistant prop
export const useSearchSharedLogic() {
return readonly({
searchPin
})
}
const useSearchWriteLogic() {
return {
searchPin
}
}
// ----------- In another file -----------
export default function useSearch() {
const { searchPin } = useSearchSharedLogic()
function searchResults() {
return searchPin.value;
}
return {
searchPin,
searchResults
};
}
Or something of this sort (Not even sure this would work correctly as written).
Point is, don't expose a single instance directly
Another point worth mentioning is that this answer takes measure to preserve unidirectional data flow pattern. Although this is a basic proven pattern for years, it's not carved in stone. As composition patterns get clearer in the close time, IMO we might see people trying to challenge this concept and returning in some sense to bidirectional pattern like in Angular 1, which at the time caused many problems and wasn't implemented well

Related

Why is this composable not reactive?

I have this composable in my app:
// src/composition-api/usePermissions.js
import { ref, readonly } from 'vue'
import { fetchData } from 'src/utils/functions/APIFunctions'
export function usePermissions() {
const permissions = ref([])
const name = ref('')
const fetchCurrentUser = () => {
fetchData('users/me').then(res => {
name.value = `${res.first_name} ${res.last_name}`
permissions.value = res.roles
})
}
return {
name: readonly(name),
permissions: readonly(permissions),
fetchCurrentUser,
}
}
FetchCurrentUser is called in the main layout component.
<template>
<!-- src/layouts/MainLayout.vue -->
<q-layout view="lHh Lpr lFf">
<!-- Redacted for brevity -->
</q-layout>
</template>
<script setup>
import { defineComponent, ref, onMounted } from "vue";
import EssentialLink from "src/components/EssentialLink.vue";
import { usePermissions } from "src/composition-api/usePermissions";
const { fetchCurrentUser } = usePermissions();
/* redacted for brevity */
onMounted(() => {
fetchCurrentUser();
});
</script>
And the state is used in other components, such as this one.
<template>
<!-- src/components/loggedUserLabel.vue -->
<div>{{ name }}</div>
</template>
<script setup>
import { usePermissions } from "src/composition-api/usePermissions.js";
const { name } = usePermissions();
</script>
The fetchCurrentUser() function is used when starting the app or when a new user logs in. I want to use this composable in other components to restrict access to some parts of the app depending on user permissions, and to display the username. However, the name and permissions properties are not reacting to changes. What could be wrong here?
If it matters, I'm using Quasar. I could use Pinia for this but I only need this kind of store-like shared state here, so it seems like overkill to add another library.
At Estus Flask's request, I have created a MCVE - and the same problem keeps happening. Note how in src/layouts/MainLayout.vue the API call is made, but the message in src/components/PokemonLabel.vue does not update, despite receiving a valid response from the Pokeapi.
I have found a similar answered question but it is about a different situation.
Thanks in advance for your help!
name and fetchCurrentUser are supposed to be used in the same component. The state of fetchCurrentUser isn't shared between components, this is by design.
In order to make the state global, it should be created once per component hierarchy, e.g.:
const name = ref('')
export function usePermissions() {
const fetchCurrentUser = ...
return {
name: readonly(name),
fetchCurrentUser,
}
}
This won't work correctly for SSR application like Quasar because there are multiple application instances for different users, but the state is created once and shared between them. In this case the state likely should be created for a hierarchy of components, e.g.:
export function setupPermissions() {
const name = ref('')
const fetchCurrentUser = ...
provide('permissionsStore', {
name: readonly(name),
fetchCurrentUser,
})
}
export function usePermissions() {
return inject('permissionsStore');
}
Where usePermissions is used in child component. And setupPermissions is used in root component, or can be rewritten as a plugin.

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.

this.$forceUpdate equivalent in Vue 3 - Composition API?

In Vue 2, instance method this.$forceUpdate() could be used to update the component manually. How can we force update component in Vue 3 - Composition API (inside setup method) ?
setup(props, context) {
const doSomething = () => {
/* how to call $forceUpdate here? */
}
return {
doSomething
}
}
Thanks, in advance.
If using Options API:
<script lang="ts">
import {getCurrentInstance, defineComponent} from 'vue'
export default defineComponent({
setup() {
const instance = getCurrentInstance();
instance?.proxy?.$forceUpdate();
}
})
</script>
If using Composition API with <script setup>
<script setup lang="ts">
import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance();
instance?.proxy?.$forceUpdate();
</script>
When I need to force an update in vue I usually add a key with a value I can change, which will then force vue to update it. That should work in vue 3 as well, though I admit I haven't ever tried it. Here's an example:
<template>
<ComponentToUpdate
:key="updateKey"
/>
</template>
<script>
export default {
data() {
return {
updateKey: 0,
};
},
methods: {
forceUpdate() {
this.updateKey += 1;
}
}
}
</script>
You can read more about it here: https://michaelnthiessen.com/key-changing-technique/
$forceUpdate is still available in Vue3, but you won't have access to it in the setup() function. If you absolutely need to use it, you can use object API component or this fancy trick...
app.component("my-component", {
template: `...`,
methods: {
forceUpdate(){
this.$forceUpdate()
}
},
setup(props) {
const instance = Vue.getCurrentInstance();
Vue.onBeforeMount(()=>{
// instance.ctx is not populated immediately
instance.ctx.forceUpdate();
})
return {doSomething};
},
})
If this seems like a ridiculous solution, trust your Judgement. Ideally your application would not rely on forceUpdate. If you are relying on it, it likely means that something is miss-configured, and that should be the first thing to resolve.

vue3 performance warning using ref

vue is throwing this message:
Vue received a Component which was made a reactive object. This can
lead to unnecessary performance overhead, and should be avoided by
marking the component with markRaw or using shallowRef instead of
ref.
<template>
<component v-for="(el, idx) in elements" :key="idx" :data="el" :is="el.component" />
</template>
setup() {
const { getters } = useStore()
const elements = ref([])
onMounted(() => {
fetchData().then((response) => {
elements.value = parseData(response)
})
})
return { parseData }
}
is there a better way to do this?
First, you should return { elements } instead of parseData in your setup i think.
I solved this issue by marking the objects as shallowRef :
import { shallowRef, ref, computed } from 'vue'
import { EditProfileForm, EditLocationForm, EditPasswordForm} from '#/components/profile/forms'
const profile = shallowRef(EditProfileForm)
const location = shallowRef(EditLocationForm)
const password = shallowRef(EditPasswordForm)
const forms = [profile, location, password]
<component v-for="(form, i) in forms" :key="i" :is="form" />
So you should shallowRef your components inside your parseData function. I tried markRaw at start, but it made the component non-reactive. Here it works perfectly.
you could manually shallowCopy the result
<component v-for="(el, idx) in elements" :key="idx" :data="el" :is="{...el.component}" />
I had the same error. I solved it with markRaw. You can read about it here!
my code :
import { markRaw } from "vue";
import Component from "./components/Component.vue";
data() {
return {
Component: markRaw(Component),
}
For me, I had defined a map in the data section.
<script>
import TheFoo from '#/TheFoo.vue';
export default {
name: 'MyComponent',
data: function () {
return {
someMap: {
key: TheFoo
}
};
}
};
</script>
The data section can be updated reactively, so I got the console errors. Moving the map to a computed fixed it.
<script>
import TheFoo from '#/TheFoo.vue';
export default {
name: 'MyComponent',
computed: {
someMap: function () {
return {
key: TheFoo
};
}
}
};
</script>
I had this warning while displaying an SVG component; from what I deduced, Vue was showing the warning because it assumes the component is reactive and in some cases the reactive object can be huge causing performance issues.
The markRaw API tells Vue not to bother about reactivity on the component, like so - markRaw(<Your-Component> or regular object)
I also meet this problem today,and here is my solution to solve it:
setup() {
const routineLayoutOption = reactive({
board: {
component: () => RoutineBoard,
},
table: {
component: () => RoutineTable,
},
flow: {
component: () => RoutineFlow,
},
});
}
I set the component variant as the result of the function.
And in the ,bind it like compoennt()
<component
:is="routineLayoutOption[currentLayout].component()"
></component>

Vuejs clone object in script tag or inside instance e.g. data

Wondering what is the best practice for such code here and what is the difference when cloning an object inside script tag or doing it in data property:
<script>
import {cloneDeep} from "lodash";
import {INVITE_USER_FORM_FIELDS} from './data';
const FORM_FIELDS = cloneDeep(INVITE_USER_FORM_FIELDS);
export default {
name: "ModalInviteCreate",
data() {
return {
FORM_FIELDS,
};
},
OR
<script>
import {cloneDeep} from "lodash";
import {INVITE_USER_FORM_FIELDS} from './data';
export default {
name: "ModalInviteCreate",
data() {
return {
FORM_FIELDS: cloneDeep(INVITE_USER_FORM_FIELDS),
};
},
The difference is the data method will run every time you create a new instance of that component. If you never need to recompute a deep clone, then option 1 is preferable since the extra clones are a waste. If you're bothering to create a deep clone though, I'm guessing it's so your components can safely mutate the object without modifying the original. So option 2 is probably the best choice, otherwise all of the component instances would all share the same object.
Here's an example to illustrate, see the console.logs.
const fakeDeepClone = name => {
console.log(`creating data for component ${name}`);
return { name };
}
const aData = fakeDeepClone('a');
const componentA = {
template: '<div>name: {{name}}</div>',
data() {
return aData
}
}
const componentB = {
template: '<div>name: {{name}}</div>',
data() {
return fakeDeepClone('b')
}
}
var app = new Vue({
el: '#app',
components: {
componentA,
componentB
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<component-a></component-a>
<component-a></component-a>
<component-b></component-b>
<component-b></component-b>
</div>