Must-Know Reusable Module Vs Component In Vue 3 Composition API

Javascript Vue.js

As a Vue.js developer, we want to keep our code clean and tidy by creating the optimal amount of reusable code.

In Vue 3 with the Composition API, we can absolutely do that by creating reusable JS modules aka hook functions and child components.

So what’s the difference between them?

When do we use one or the other?

Let’s find out!

JavaScript module aka hook function is a great way to separate business logic from the Vue component.

It’s completely reusable and UI independent.

If you’re coming from MVC pattern, this would be M (Model) part of it.

Vue components that are not page-based, meaning no route attached to it, aka Child components, are great when we want to reuse HTML template code aka UI that can be used in multiple parent components.

Now you have a better understanding of not only the differences, but also which one to use when.

I’m going to demonstrate with some example code throughout this article to show cases of when to use them.

Note: This article is intermediate and you should have the basic knowledge of using Vue.js or a similar framework.

Get A List Of Products

I assume you already know how to get Up and Running With Vue JS 3 Project Using Vue CLI

Let’s say we have a ProductList.vue page-based component in our application handling async operation to get a list of products.

I used Firebase in the example below for the async operations.

I have an article on how to install Firebase to your vue project as well as on how to make Cloud Firestore queries if you’re interested.

First, create a product reactive array and call loadProduct() function inside onMounted() lifecycle method.

Recommended
Must-Know Ref vs Reactive Differences In Vue 3 Composition API

import firebase from "firebase";
import { onMounted, reactive } from "vue";
export default {

  setup() {

    const products = reactive([]);

    onMounted(() => {
      loadProducts();
    });

    const loadProducts = async () {
      try {
        const productsSnap = await firebase
          .firestore()
          .collection("products")
          .orderBy("brand", "asc")
          .get();

        productsSnap.forEach((doc) => {
          let product = doc.data();
          product.id = doc.id;
          products.push(product);
        });

      } catch (e) {
        console.log("Error Loading Products");
      }
    }

    return {
      products,
    };
  },
};

Inside the loadProducts() declaration, use Firebase query to get a list of products that I’ve already stored in the Firebase Cloud Firestore database.

Finally return the products array to use in the template.

Each product object has two keys that are:

  • title
  • upc

Nothing fancy.

This works great!

As the project evolves, let’s say I’ll need to create an inventory component where I want to use a list of products as a drop-down, for example, to select a product to create an inventory from.

I’ll have to rewrite loadProducts() code which I do not want to do.

With Vue 3 Composition API, we can easily extract the async code into a UI-independent JavaScript module and can reuse it wherever we want in our application.

Let’s see how to do that.

Recommended
Must-Know Ref vs Reactive Differences In Vue 3 Composition API

Reusable Product.js Module

Create a Product.js file inside the src/modules folder which is where all of my product-related async operations will go such as loadProducts, addProduct, updateProduct, deleteProduct, etc.

Let’s see how to extract the loadProducts() code from the ProductList.vue to Product.js

Import Firebase as we’ll be using it for CRUD async operations.

Then declare useProduct() function as well as reactive state object inside the function which will have one property in it called products that is a type of array.

import { reactive, toRefs } from "vue";
import firebase from "firebase";

export default function useProduct() {
  const state = reactive({
    products: [],
  });

  const loadProducts = async() => {
    try {
      const productsSnap = await firebase.firestore().collection("products").orderBy("brand", "asc").get();
      productsSnap.forEach(doc => {
        let product = doc.data();
        product.id = doc.id;
        state.products.push(product);
      });
    } catch (e) {
      console.log("Error Loading Products")
    }
  }
  return { ...toRefs(state),
    loadProducts
  }
}

Then, create the loadProducts() function which gets the products data from the products collection in Cloud Firestore and gets the snapshot of it.

Loop through them and push each product into the products array.

Finally, return the state and loadProduct.

The reason I wrapped state with toRefs is to convert the reactive object into a plain JavaScript object so that I can destructure it when needed.

Now we have the Product.js module ready to use inside the ProductList.vue or any component that needs a list of products in our app.

ProductList.vue

Now, we can easily get a list of products like before with just four steps.

First, import the product module at the top.

Secondly, call useProduct() function and destructure products and loadProducts inside the setup() function.

Then, invoke loadProducts() inside the onMounted() lifecycle method.

import { onMounted } from "vue";
import useProduct from "@/modules/product";
export default {
  setup() {
    const { products, loadProducts } = useProduct();
    onMounted(async () => {
      await loadProducts();
    });
    return {
      products,
    };
  },
};

Finally, return the products which is the same as before.

At this stage, products are available in the template to use and we can loop through them using v-for to show however we want.

Recommended
Vue + Firestore ← Build A Simple CRUD App with Authentication

You can see how easy it is to create a module like Product.js to organize reusable UI-independent functionalities. 🙂

One more benefit of using modules like Product.js is to use them as state management as well.

Modules As State Management

To use modules as state management, all we need to do is to declare the state object model outside of the useProduct() function which then becomes a global variable.

import { reactive, toRefs } from "vue";
import firebase from "firebase";

 const state = reactive({
        products: [],
 });

export default function useProduct() {
  ...
}

This way, I do not have to fetch data from Firebase every time I go to the ProductList.vue route.

Let’s take a look at creating a reusable component.

Reusable Vue Component

There are some scenarios where you would need to reuse template code.

For example, let’s say we have two views in our application; one for creating a new product (ProductAdd.vue) and the other one for editing a product (ProductEdit.vue).

Both will have the same amount of form elements which is one of the great uses for creating reusable Vue components.

Let’s create a component called ProductForm.vue inside the src/components folder.

This will be a child component having form elements which then will be used by ProductAdd.vue and ProductEdit.vue files.

Recommended
Must-Know Property Creation Differences in Vue 2 and Vue 3

ProductForm.vue

Here is a simple form template code in the ProductForm.vue component that has two input fields that are title and upc.

<template>
  <form @submit.prevent>
    <h1>Create Product</h1>
    <div class="field">
      <label>Title</label>
      <input type="text" v-model="product.title" />
    </div>
    <div class="field">
        <label>UPC</label>
        <input type="text" v-model="product.upc" />
      </div>
    </div>
    <button class="ui button blue tiny" @click="productSaveButtonPressed">Save Product</button>
  </form>
</template>

There is one button with the callback function.

Nothing is fancy.

Now, let’s declare the product object inside the setup() function that’s already bound to the input field in the template code above.

<script>
import { reactive } from "vue";
export default {
  
  setup(_, {emit}) {

    const product = reactive({
      title: "",
      upc: 0,
    });

    const saveProductButtonPressed = () => {
      emit("product", product);
    };

    return { product, saveProductButtonPressed };
  },
};
</script>

Every time, when a signup button is pressed, we want to send form data to the caller (parent component) using the emit method which can be destructured from the second argument called context in the setup function.

Finally, return product and saveProductButtonPressed so that template code will have access to them.

Now that we have a reusable view component, let’s use it inside ProductAdd.vue component.

Recommended
Vue JS 3 Composition API Form Validation – Signup & Login Pages

ProductAdd.vue

Let’s use the ProductForm.vue child component inside the ProductAdd.vue component.

First, import the ProductForm.vue component.

Then, add it to the components object.

<template>
  <ProductForm @product="getProductData" />
</template>

<script>
import ProductForm from "@/components/ProductForm";

export default {
  components: {
    ProductForm,
  },
  setup() {

    const getProductData = (product) => {
      console.log(product)
    };

    return { getProductData };
  },
};
</script>

Finally, capture the product data that is emitted by ProductForm.vue in the template using the getProductData() function and the actual value will be in the argument of the function signature.

Now that we have form data, let’s actually add the product to the Database.

Recommended
Vue JS 3 Composition API Form Validation – Signup & Login Pages

Add Product In Product.js

Now, create the addProduct() function inside Product.js that I’ve already mentioned where all of the product-related async operations go in.

...
export default function useProducts() {
   ...
    const addProduct = async (product) => {
        try {
            await firebase.firestore().collection("products").doc().set(product);
        } catch (e) {
            console.log(e.message);
        }
    }
    return { ...toRefs(state), loadProducts, addProduct }
}

Now the addProduct() function is ready to use inside ProductAdd.vue component, we can use it to actually insert data to the Firebase database.

<template>
  <ProductForm @product="getProductData" />
</template>

<script>
import ProductForm from "@/components/ProductForm";
import useProduct from "@/modules/product";

export default {
  components: {
    ProductForm,
  },
  setup() {

    const getProductData = async (product) => {
      await addProduct(product);
    };

    return { getProductData };
  },
};
</script>

Pretty simple!

Let’s see how to use the same ProductForm.vue component for editing a product.

ProductEdit.vue

Unlike ProductAdd.vue, we’ll need to send product data that we want to edit to ProductForm.vue file first in order to prepopulate data in the input fields.

When a user presses submit button, emit the updated form data to the ProductEdit.vue parent component to actually save it to the Database.

<template>
  <ProductForm
    @product="getProductData"
  />
</template>

<script>
import ProductForm from "@/components/ProductForm";

export default {
  components: {
    ProductForm,
  },
  setup() {

    const getProductData = (product) => {
      console.log(product)
    };

    return { getProductData };
  },
};
</script>

Now we need to create two functions inside the Product.js module to handle editing a product properly.

One function is to load a product based on a product ID that the user wants to edit. The other function is to actually update that product with new data to the database.

Recommended
Vue JS 3 Composition API Form Validation – Signup & Login Pages

Load And Edit Product In Product.js

Add a product property which is a type of object to the state object.

Then, define loadProduct() which will take the product ID as an argument and get product data from Firebase, and set it to the product object.

...
const state = reactive({
    products: [],
    product: {},
});

export default function useProduct() {
    ...
   const loadProduct = async (id) => {
        try {
            const productSnap = await firebase.firestore().collection("products").doc(id).get();
            state.product = productSnap.data();
        } catch (e) {
            console.log(e.message);
        }

    }

    const updateProduct = async (id, product) => {
        try {
            await firebase.firestore().collection("products").doc(id).update(product);
        } catch (e) {
            console.log(e.message);
        }
    }
    return { ...toRefs(state), loadProducts, loadProduct, updateProduct, addProduct }

}

Next, one is updateProduct() which will take two arguments: product ID and the actual product data that needs to be updated.

Finally, return them so that we can access them in the ProductEdit.vue component.

Now, let’s get the product we want to edit and populate the product data to the form fields.

Update A Product

Assume that we can get the product ID from the URL when navigating to the ProductEdit.vue component and invoke the loadProduct() function with a product inside the onMounted() lifecycle method.

<template>
  <ProductForm
    :editProduct="product"
    @product="getProductData"
  />
</template>

<script>
import { onMounted } from "vue";
import { useRoute } from "vue-router";
import useProduct from "@/modules/product";
import ProductForm from "@/components/ProductForm";

export default {
  components: {
    ProductForm,
  },
  setup() {
    const route = useRoute();

     const {
      product,
      loadProduct,
    } = useProduct();

    onMounted(async () => {
      await loadProoduct(route.params.id) 
    });

    const getProductData = (product) => {
      console.log(product)
    };

    return { product, getProductData };
  },
};
</script>

Once the product data is received in the ProductEdit.vue component, send it to the ProductForm.vue template as a value of :editProduct attribute.

Populated Product Data In The Form

In the ProductForm.vue, capture the editProduct property using props object.

Then, loop through editProduct properties and set an appropriate value to the product object using key.

import { onMounted, reactive } from "vue";

export default {

 props: {
    editProduct: Object,
 },

  onMounted(() => {
      if (props.editProduct) {
        for (let key in props.editProduct) {
          product[key] = props.editProduct[key];
      }
   }
...
}

At this stage, all the input fields will be pre-populated with product data.

Update Product Data

At this stage the user will be able to change any of the product data.

As soon as the save button is pressed, product data will be emitted to its caller, in this case, the ProductEdit.vue component.

Similar to the previous example, we can use the getProductData() function to capture the data.

...
     const {
      product,
      loadProduct,
      updateProduct
    } = useProduct();

    ...

    const getProductData = (product) => {
      await updateProduct(route.params.id, product);
    };
...
  },
};
</script>

Finally, call the updateProduct() function with product ID and the product object that we want to update with new data to the database.

That’s it!

Conclusion

As you can see, creating reusable code will keep the application clean and tidy as well as how easy it’s by using Vue 3 Composition API.

To recap, JavaScript module aka hook function is a great way to separate logic from the Vue component. It’s completely reusable and UI-independent.

Vue components, on the other hand, are great when you want to reuse template code, aka child component, that can be used in multiple parent components.

Now you have a better understanding of not only the differences but also which one to use.

I hope it helps!

Feel free to connect with me if you’ve any suggestions or feedback. I always like to improve my articles and provide great value to the readers.

Happy Coding!