Top 3 Composition API pitfalls

Three common mistakes when using the Composition API in Vue.js, explaining what they are, why they occur, and how to solve them


Whether you like it or not, the Composition API is the new standard for Vue.js. As with any significant novelty, whether due to lack of attention or documentation, its adoption can lead to mistakes that over time become bad practices or examples of incorrect use.

In this DevTip, we will examine three of these common mistakes: what they are, why they occur, and how you can fix them once and for all.

Problem 1: Incorrect use of reactive

This is a classic and one of the reasons why some Vue developers prefer to always use ref, instead of reactive.

First, we cannot use primitive values with reactive, only objects such as Object, Array, Map, and others. Second, once the reference is created, it cannot be broken. This means that this reference must not change, but can mutate.

Finally, and as a consequence of that, reactive cannot be destructured without a little extra help. Let me explain:

TS
import { reactive } from 'vue';
const state = reactive({
  count: 0,
  name: 'John',
});
const { name } = state;
state.name = 'Juan';
console.log(name, state.name); // John Juan

As you can see, once we perform the destructuring, the reference is lost and therefore name is not updated.

To destructure reactive you can use the helper toRefs, which will convert the properties of an object created with reactive into reactive references (generated with ref).

TS
import { reactive, toRefs } from 'vue';
const state = reactive({
  count: 0,
  name: 'John',
});
const { name } = toRefs(state);
state.name = 'Juan';
console.log(name.value, state.name); // Juan Juan

As you can see, now name is a reactive reference linked to the reactive object. It is actually now an object, as it is accessed through value. As long as this remains the case, they will always stay synchronized.

Problem 2: Too much logic in your components

One of the most positive aspects of the Options API is that it forces us to structure logic in different parts. Thanks to it, your code is more organized. When stepping up to the Composition API, our code can seem disorganized, as we now have more freedom. Now the responsibility falls on us to follow a code structure pattern.

Another (in my point of view) mistake is to group a lot of related logic in one component. Many times we forget that we can now use reactive APIs (ref, reactive and others) outside of them.

An example:

TS
import { ref, reactive, onMounted, watch } from 'vue';
const data = ref(null);
const error = ref(null);
const searchTerm = ref('');
const state = reactive({ count: 0 });
const fetchData = async () => {
  try {
    const response = await fetch(
      `https://api.example.com/data?search=${searchTerm.value}`
    );
    data.value = await response.json();
  } catch (err) {
    error.value = err;
  }
};
onMounted(fetchData);
watch(searchTerm, fetchData);

There is nothing negative in the above code, but in most cases it is interesting to extract functions and logic that can be reused elsewhere. In this case, the fetchData function becomes the useFetchData composable:

TS
import { ref, onMounted, watch } from 'vue';
export function useFetchData(searchTerm) {
  const data = ref(null);
  const error = ref(null);
  const fetchData = async () => {
    try {
      const response = await fetch(
        `https://api.example.com/data?search=${searchTerm.value}`
      );
      data.value = await response.json();
    } catch (err) {
      error.value = err;
    }
  };
  onMounted(fetchData);
  watch(searchTerm, fetchData);
  return { data, error };
}

Using it is as simple as:

TS
import { useFetchData } from '@/composables';
const searchTerm = ref('');
const { data, error } = useFetchData(searchTerm);

Problem 3: Expect reactivity when it is not possible

Last but not least, we have those situations where it is expected that something continues to be reactive when it really is not. This false assumption makes this type of error the most difficult to debug.

Suppose we want to pass part of the reactive properties of a component to another place —like a composable— and expect them to continue to be reactive.

A super-simplified example:

TS
import { watch } from 'vue';
export function useTest(msg) {
  watch(
    msg,
    (newVal) => {
      console.log(newVal);
    },
    { immediate: true }
  );
}

Now we make use of the composable useTest in our component:

Vue
<script setup>
  import { useTest } from '@/composables/useTest'
  const props = defineProps<{
    msg: string
  }>()
  useTest(props.msg)
</script>

Do you see the problem?. At first glance, it is not simple.

The problem is that props.msg is not a reactive reference, it is the result of it. The final value. Just like if you refer to the value property of something created with ref. What you are passing to useTest is a string, not an object.

In fact, if you try to put the above code into use, Vue itself will give you a warning message:

Vue warn]: Invalid watch source: You did it! A watch source can only be a
getter/effect function, a ref, a reactive object, or an array of these types.

What can we do about it?. Actually you already know: toRefs. Again. In this way, you create a reactive reference (again, with ref) using the value of props.msg:

Vue
<script setup lang="ts">
  import { toRefs } from "vue"
  ...
  const { msg } = toRefs(props)
  useTest(msg)
</script>

Now everything works as you imagine, and any change in props.msg will trigger reactivity, with useTest being notified of the mutation.

Conclusions

In summary, the use of the Composition API in Vue.js can be a big change, but it can also bring many benefits in terms of flexibility and ability to reuse logic. However, as with any tool, it is important to use it correctly to avoid falling into common traps and mistakes.

By understanding how to properly handle reactive objects, structure your code efficiently, and handle reactivity, you can make the most of this powerful feature of Vue.js.

If you are interested in learning it, take a look at the Composition API Workshop.

Juan Andrés Núñez
Juan Andrés Núñez
Frontend Engineer. Vue.js Specialist. Professional Teacher. Stoic.