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 with v-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:

  1. Create another non-searchable variant of this component.
  2. 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”.

Join The Newsletter

Subscribe to get notified of my latest content