Skip to content

Latest commit

 

History

History
369 lines (261 loc) · 9.22 KB

course-vue-3-composition-api.md

File metadata and controls

369 lines (261 loc) · 9.22 KB

All about Vue 3 Composition API

The setup method

It is the entry point.

It takes 2 parameters:

  • a list of props
  • a context object

You can define the props above the setup method in the same way we do it in the Options API. But later in this article, we review the defineProps way with TypeScript.

Inside the setup method, we accuess the props using the first parameter:

    props: {
      name: String,
      price: Number
    },
    setup(props){
      console.log(props.name);
      console.log(props.price);
    }

We can destructure the context to extract attr, slots and emits for example.

    setup(props, {attrs, slots, emit}){
      /* your code here */
    }

We can use whatever we want from the context. For example, we can use emit only.

Reactivity is not explicit, you have to tell Vue

For primitives

Primitives are string, integers, boolean.

A variable in the setup method is not reactive by default.

Using ref, you can tell Vue it is.

import { ref } from "vue";
export default {
  setup() {
    const name = ref("The Snazzy Burger");
    return { name };
  },
};

Without ref, you could not edit name in Vue DevTools and have it update in the UI.

ref is a reactive reference.

pass-by-ref-vs-pass-by-value

Using a ref variable, you will need to use .value to read or modify it within a setup method.

In the template or lifecycle hooks like created() where Vue unwrap the ref.

IMPORTANT: the reason why you should ALWAYS declare variable in setup as const is to prevent breaking the reactivity by using let and assigning a non-ref value to your initially declared ref variable.

For non primitives

Non-primitives are objects and arrays.

You can use ref, but you also have reactive.

import { ref, reactive } from "vue";
export default {
  setup() {
    const appName = ref("The Snazzy Burger");
    const meal = reactive({ name: "Hamburger", price: 5 });
    return { appName, meal };
  },
};

One advantage of reactive is that it elimates the need to use .value.

See the docs for more cool stuff about reactivity.

However, one of the trap of reactive is that you may assign the variable a complete other value and you will loose the reactivity.

Using ref guarantee it doesn't happen.

How to choose ref or reactive

See the lesson "Refs vs Reactive With the Vue 3 Composition API" for the advantages and disavantages of each.

I personnally use exclusively ref.

Using computed

It is very similar to the Options API

    import { computed } from "vue";
  export default {
    props: {
      price: Number
    },
    setup(props,){
      const prettyPrice = computed(() => `$${props.price.toFixed(2)}`);

      return {
        prettyPrice,
      }
    }

But when using composables, it will provide a new advantage.

Watching a reactive array

ALWAYS copy the reactive array, otherwise, you will work on a reference, not a true copy.

//newCart and oldCart point to the same reference
watch(cart, (newCart, oldCart) => console.log(newCart, oldCart));
watch(
  //create a copy of the reactive array
  () => [...cart],
  //newCart amd oldCart point to different references
  (newCart, oldCart) => console.log(newCart, oldCart)
);

This is true for ref or reactive.

watch vs watchEffect

watchEffect:

  • fires immediatly
  • doesn't need to be told what it depends on by passing the data as first attribut
  • doesn't have an old value

For example, with watch, we write:

import { watch } from "vue";
export default {
  setup() {
    const hideCartOnAddItem = watch(
      () => [...cart],
      (newCart, oldCart) => alert(newCart.join("\n"))
    );
  },
};

With watchEffect, it becomes:

import { watchEffect } from "vue";
export default {
  setup() {
    const hideCartOnAddItem = watchEffect(() => alert(cart.join("\n")));
  },
};

So when to use either one? Ask yourself:

  • Do I need access to the old value?
  • Will it be a problem if the callback fires inmediatly?

What about passing variables from component N to component N+2 without using a prop on component N+1

Use the methods provide and inject.

provide defines in the component N the name and value of the variable.

inject reads from component N+2 or deeper the variable's value defined in provide by component N.

A provided variable can be made reactive.

Note: you cannot inject a variable not provided by a parent. However, inject takes a second argument to prevent broken code.

const currencySymbol = inject("currencySymbol", ref("$"));

PS: using TypeScript, you can create an enum to list the names of those types of variables to avoid mistyping the variable name in the inject method.

Using lifecycle hooks in Composition API

It is as simple as extracting the hook on the import, on as-you-need basis.

See the docs for the list of methods supported.

beforeCreate and created are exception because setup is called around those two.

What is the purpose of setup on the script tag

It is implicity return all variables and methods to become useable in the template.

One caviat about the setup attribut: only single file component can use it.

Avoid mutation of props that are objects

To do so, use the spread operator:

const props = defineProps({
  post: Object,
});
const post = { ...props.post };

Use composables

A composable is a file, external to the components, that you can use in any component.

The convention is to:

  • put the composables in a dedicated folder composables in the src directory.
  • name the composable with the prefix use which is a convention in VueJs community to let people know it is a composable.

For example:

import { ref } from "vue";

export default function usePost() {
  const posts = ref([]);

  const fetchAll = async () => {
    const response = await fetch("https://jsonplaceholder.typicode.com/posts");
    posts.value = await response.json();
  };

  return {
    posts,
    fetchAll,
  };
}

Asynchronous Data and the Composition API

Using then()

//fetchUser is dependant on the fetchPost, so we wait it resolves.
fetchPost(route.params.id).then(() => {
  fetchUser(post.value.userId);
});

Using async / await

//fetchUser is dependant on the fetchPost, so we wait it resolves.
const post = (ref < Post) | (null > null);
const user = (ref < User) | (null > null);

const fetData = async () => {
  post.value = await fetchPost(route.params.id);
  user.value = await fetchUser(post.value.userId);
};

Using async self-invoking anonymous function

(async () => {
  await fetchPost(route.params.id);
  fetchUser(post.value.userId);
})(); //the final () is the call of the function.

Use a watch

To keep the logical concerns together, you can use a watch:

const { item: post, fetchOne: fetchPost } = useResource("posts");

const route = useRoute();
fetchPost(route.params.id);

const { item: user, fetchOne: fetchUser } = useResource("users");

watch(
  //when the post changes...
  () => ({ ...post.value }),
  //call the user api
  () => {
    fetchUser(post.value.userId);
  }
);

Use of suspense

IMPORTANT: on January 9th 2024, it is still under an experimental status. See the docs. However, many projects use it in production (Nuxt 3 for example).

The usage is the following:

<!--
    the async is just "sugar" syntax, not the same as the asnc keyword on a asynchronous function...
-->
<script setup async>
  import { watch } from "vue";
  import { useRoute } from "vue-router";
  import useResource from '../composables/useResource';

  const { item: post, fetchOne: fetchPost } = useResource('posts');

  const route = useRoute();
  //...mark the function to wait on...
  await fetchPost(route.params.id);

  const { item: user, fetchOne: fetchUser } = useResource('users');
  //...and fetchUser is same to call now since fetchPost is awaited.
  fetchUser(post.value.userId)
</script>

To work, you will need to use the suspense component higher up in the components tree, e.g. App.vue:

<!-- in the template of App.vue -->

<suspense>
  <template #default>
    <router-view></router-view>
  </template>
  <template #fallback>
    <p>Loading...</p>
  </template>
</suspense>

It is important to note that Both slots only allow for one immediate child node.

But with router-view, it is more like this that you need to implement it:

<!-- in the template of App.vue -->

<router-view v-slot="{ Component }">
  <template v-if="Component">
    <suspense>
      <template #default>
        <component :is="Component"></component>
      </template>
      <template #fallback>
        <p>Loading...</p>
      </template>
    </suspense>
  </template>
</router-view>

Read more in the documentation.