Conditional Vue.js Compositions
One often ignored aspect of the Vue.js composition API is that you can call it conditionally, this gives it huge flexibility over something like React hooks where the execution order is important. This will come in handy in the various scenarios that I will explore.
The Problem
Letâs assume we have a searchable list composable like this one:
js/*
* Searches an array of objects with the given by matching each item's properties
*/
function useSearchableList({ items, query, searchableProps }) {
const results = computed(() => {
// check if we have an actual value to use for search
if (!query.value) {
return items.value;
}
// build a Regex to search the item object properties
const matchRE = new RegExp(query.value, 'i');
return items.value.filter((item) => {
return searchableProps.some((prop) =>
matchRE.test(String(item[prop])),
);
});
});
return results;
}
What this composable does is it takes these arguments:
list
: A reactive reference to an array of object items.query
: A reactive reference to your search query, usually bound to input withv-model
.searchableProps
: An array (non-reactive) of strings representing the properties that should be looked at for matching.
This composable itself is an isolated piece of logic that could be used in a lot of components. like a multi-select dropdown with search capabilities similar to the vue-multiselect component.
Iâve kept it simple but you can spice it up with Fuzzy searching with libraries like fuse
or fuzzy-search
.
Now letâs say we are building a searchable list component with the search capability. I will use a basic UI here, but I will provide a more complete example later on.
Heads up
I will be using the cool SFC setup syntax to keep things tidy and short
vue<template>
<div>
<input v-model="query" type="text" />
<ul>
<li v-for="item in results" :key="item.id">
{{ item.name }} {{ item.email }} {{ item.phone }}
</li>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useSearchableList } from '@/features/search';
const items = ref([
{
id: 1,
name: 'First',
email: 'first@example.com',
phone: '0701740605',
},
{
id: 2,
name: 'Second',
email: 'second@test.com',
phone: '0313900600',
},
{
id: 3,
name: 'Third',
email: 'third@random.com',
phone: '0406280400',
},
]);
const query = ref('');
const searchableProps = ['email', 'name', 'phone'];
const results = useSearchableList({
items,
query,
searchableProps,
});
</script>
This works really well, but letâs say there are some select inputs that you donât want to be searchable at all. You have 2 approaches here:
- Create another non-searchable variant of this component.
- Use the same component and add a configuration property to separate the 2 behaviors.
Since both components would be visually similar, it makes sense to go with option #2.
Here is what it will look like:
vue<template>
<div>
<input v-if="searchable" v-model="query" type="text" />
<ul>
<li v-for="item in results" :key="item.id">
{{ item.name }} {{ item.email }} {{ item.phone }}
</li>
</ul>
</div>
</template>
<script setup>
import { ref, defineProps } from 'vue';
import { useSearchableList } from '@/features/search';
const props = defineProps({
searchable: {
default: false,
type: Boolean,
},
});
const items = ref([
// ...
]);
const query = ref('');
const searchableProps = ['email', 'name', 'phone'];
const results = useSearchableList({
items,
query,
searchableProps,
});
</script>
The searchable
property will act as a configuration for the component to behave as a searchable list or not.
I like my non-required boolean properties to be false
by default, so that means the default behavior for my list is to be unsearchable. Feel free to do what you find suitable.
This is an example of using both variants of the component:
vue<template>
<List />
<List searchable />
</template>
<script setup>
import List from '@/components/List.vue';
</script>
Since our component is very simple, we donât really care whether useSearchableList
gets called or not. as long as there is no way to mutate the query
value it should behave as searchable. But letâs say you are doing more complex things in that composable.
Letâs assume it involves setting up a lot of listeners and watchers and it may hurt your performance if sprinkled across the app. Is there a way to conditionally call it?
Conditionally Calling Composables
The neat thing about Vue.js composition API, is it allows us to do the following:
jsimport { ref, watch, inject, computed } from 'vue';
if (condition) {
const count = ref(0);
}
if (condition) {
watch(something, doSomething);
}
if (condition) {
const double = computed(...);
}
if (condition) {
const somethingToInject = inject('someKey');
}
This allows us to be pretty much flexible about when to call a composition API function which can be an underutilized aspect.
The hard requirement for this is it only works statically. Meaning you cannot suddenly decide to call the composables based on runtime dynamic conditions. It must be deterministic in the setup function and cannot be changed afterward.
Meaning it will work only for these kinds of situations and can be driven by static configuration props which are not meant to be reactive.
So letâs put this to practice on the useSearchableList
.
Based on the searchable
prop, we want to call useSearchableList
or not.
vue<template>
<div>
<input v-if="searchable" v-model="query" type="text" />
<ul>
<li v-for="item in results" :key="item.id">
{{ item.name }} {{ item.email }} {{ item.phone }}
</li>
</ul>
</div>
</template>
<script setup>
import { ref, defineProps } from 'vue';
import { useSearchableList } from '@/features/search';
const props = defineProps({
searchable: {
default: false,
type: Boolean,
}
});
const items = ref([
// ...
]);
if (props.searchable) {
const query = ref('');
const searchableProps = ['email', 'name', 'phone'];
const results = useSearchableList({
items,
query,
searchableProps,
}
});
</script>
But there is an issue here, the query
and results
are used in the template which will defiantly log a few errors about them not being undefined.
You need to be careful when using this technique with templates because the template is compiled and it will have to find references to those variables that may or may not exist for optimization among other things.
If you are using render functions then you can work around this or maybe JSX. However, the mainstream usage of Vue is with SFC templates so letâs address that.
We can fix it easily by introducing suitable fallbacks for these composables and re-arranging some things. Looking at how our List.vue
works we can do the following:
First, letâs have the query
ref present at all times, it is a simple ref so it wonât be harmful. Moving it out of the if
statement should do the trick:
jsconst items = ref([
// ...
]);
const query = ref('');
if (props.searchable) {
const searchableProps = ['email', 'name', 'phone'];
const results = useSearchableList({
items,
query,
searchableProps,
});
}
Secondly, for the results
, it can be a little bit complicated but first letâs ask ourselves: what the value of results
would be if the search feature is disabled?
It would be equal to the original items
ref since all items will be returned without any kind of filtering and since both have the same type we can easily rewrite our code like this:
jsconst items = ref([
// ...
]);
// define results as a mutable variable with `let`
let results = items;
const query = ref('');
if (props.searchable) {
const searchableProps = ['email', 'name', 'phone'];
// override the previous ref
results = useSearchableList({
items,
query,
searchableProps,
});
}
This is one of the cases where I find let
useful with reactive references over const
which is probably the default for a lot of people. Having this flexibility of swapping out entire references is what makes this possible in Vue.js. We could call this âref switcherooâ, but really it is a kind of introducing a mock.
Now our component wonât call useSearchableList
at all if it doesnât have to, which in very large applications and complex UIs, can be a blessing.
At any case here is the finished example:
This technique can be applied in nested composables and not just components, for example, if we have a composable called useProducts
that combines our composable with an API call to fetch some products we could inline the switcheroo trick inside the higher-level composable.
jsimport { useSearchableList } from '@/features/search';
function useProducts({ query, searchable }) {
const products = ref([]);
onMounted(() => {
// Fetch the products
});
let productsMatched = products;
if (searchable) {
productsMatched = useSearchableList({
items: products,
query,
searchableProps: ['name', 'sku', 'brand'],
});
}
return productsMatched;
}
Practicality
Should we even use this technique?
The composition APIsâ usage either written by you or provided by 3rd party, usually is coupled with your components or other 3rd party components.
It could be beneficial to control when to call them. Especially if they are doing a lot of things, why execute some code that wonât be used?
But donât use it just because you think some composable is doing a lot of work. Measure, evaluate and then test the impact of the changes. Chances are, you donât really need this.
I think these couple of situations could use this technique with limited success:
- Data-tables or Sheet cells (There is complex formatting, validation, and features available to only some types of cells/columns).
- User-Drawable canvases (e.g: flow chart builders) as not all features can be applied to all shapes.
Example: Input Validation
Since I am the creator of vee-validate, I can testify to the amount of work done inside useField
and useForm
composable. It roughly does the following:
- Looks up a few injections (provide/inject)
- Sets up some computed properties (computed)
- Sets up some reactive values (ref and reactive)
- Sets up some watchers (watch and watchEffect)
Other validation libraries are probably doing similar things to get the validation logic working. You may feel âstuckâ because youâve coupled the validation composable into your input components with no real way to âdisableâ it.
Imagine calling these up every time you set up a field that you are not going to validate, imagine that field repeats frequently like in a table that has no validation. Avoiding this could have memory usage enhancements and faster rendering depending on the size of your application.
So, how would we do the switcheroo with a 3rd party library like vee-validate?
I would say, use TypeScript
. The nature of it using interfaces or duck-typing plays very well with what we want to do.
Also, you should only focus on what you use out of those composables, donât try to mock the whole library.
So for example letâs assume our component is simple and uses vee-validateâs useField
to provide a tracked model and display some error messages:
vue<script setup>
import { defineProps, ref } from 'vue';
import { useField } from 'vee-validate';
const props = defineProps({
name: {
type: String,
required: true,
},
rules: {
type: null,
default: null,
},
novalidate: {
type: Boolean,
default: false,
},
});
let value;
let errorMessage;
if (props.novalidate) {
value = ref('');
// no point in making errorMessage reactive if no validation will be done.
errorMessage = '';
} else {
const field = useField(props.name, props.rules);
value = field.value;
errorMessage = field.errorMessage;
}
</script>
So just mocking the value
and errorMessage
with safe fallbacks is enough for this use case.
Of course, it still depends on your usage and what you actually use of that composable, if itâs way too much to mock. Then consider using different components, but for a lot of use-cases, this is doable.
I need to remind you that we cannot suddenly change our mind and decide this field should be validated, the setup
only ever runs once and this is the hard requirement for this technique to work.
Cleaning up
You could clean up this approach by having âmocked-composablesâ versions for those conditional composables, for example:
jsexport function useSearchableListMock({
list,
query,
searchableProps,
}) {
return list;
}
export function usePartiallyMockedField(name) {
const value = ref('');
return {
value,
errorMessage: '',
};
}
Another approach is you could move the mocking aspect into the composable function itself, but to me, that breaks the âisolationâ aspect of the composable. What is the point of having useSearchableList
composable that has search turned off by default?
A level up of this approach that has always worked even before the composition API, is to swap entire components based on these features. For example, we could have ValidatedField
and BasicField
and then a third component that selects either based on the feature:
vue<template>
<BasicField v-if="novalidate" v-model="value" />
<ValidatedField v-else v-model="value" />
</template>
<script setup>
import ValidatedField from '@/components/ValidatedField.vue';
import BasicField from '@/components/BasicField.vue';
defineProps({
novalidate: {
type: Boolean,
default: false,
},
});
const value = ref('');
</script>
You could also use the dynamic <component />
to render either but both components should have similar APIs or have a subset/superset relationship between their APIs.
Conclusion
This is more of an experiment so do not take this into production without making sure it works well for your use case.
I want to emphasize where I personally use this: In components where complex features/composables can be turned on/off.
Vue.js is very efficient, so measure carefully before you decide that some composable is worth âswitchingâ.