Connecting UI Components to the Vuex Store | Full-Stack Google Contacts Clone with AdonisJS Framework (Node.js) and Quasar Framework (Vue.js)

Connecting UI Components to the Vuex Store | Full-Stack Google Contacts Clone with AdonisJS Framework (Node.js) and Quasar Framework (Vue.js)

In this lesson, we will connect our user interface components to the Vuex store. The components are the ui/src/pages/Index.vue which is the homepage component used for displaying the contacts table, ui/src/pages/contacts/CreateContact.vue used for creating and editing a contact, and ui/src/pages/contacts/ViewContact.vue used for displaying the details of a contact.

We will continue with our git branch: 17-connect-components-to-vuex-store. If you aren't using that branch, do the following:

# Make sure you are within your project
git checkout 17-connect-components-to-vuex-store

Pointing the ui/src/pages/Index.vue component at the Vuex Store

Open the ui/src/pages/Index.vue file. Refer to this snapshot of the file. Copy-and-paste the content of the snapshot into the ui/src/pages/Index.vue file. The template section remains on changed. So we focus on the script section.

From Line 127 to 129, we disable some eslint rules for the entire script section. This is because the types of Vuex methods and modules are not compatible when they interact.

From Line 130 to 138, we import additional hooks and types from vue. At Line 139, we import the Contact interface from ui/src/types.ts.

At Line 141, we import the useStore hook/function from the Vuex store. The useStore hook will be used to instantiate the store inside our component.

At Line 147, we call useStore() to initialise our store.

At Line 148, we make the loading ref to be true by default. This ensures that when the contacts table is rendered, the loading indicator will be shown by default.

At Line 149, we set pageSize to 50. This is the number of rows which will be fetch for each page of contacts requested from the API server.

At Line 150, we initialise the nextPage ref to 1. The first page to be first is page 1. This value will be incremented after each page of contacts is fetched.

At Line 151, we define the tableRef which will be used to store the reference to the QTable component. This tableRef will be used later to call methods defined by QTable.

At Line 153, we replace the rows computed ref with the contacts computed ref. The contacts computed ref is used to automatically fetch and store the value of the contacts/contactList Vuex getter. The contacts/contactList getter contains all the contacts currently fetched from the API server for display on the contacts table.

    const contacts = computed(
      () => store.getters["contacts/contactList"] as Contact[]
    );

At Line 157, we define a computed ref totalContacts to store the value of the contacts/totalContacts getter.

    const totalContacts = computed(
      () => store.getters["contacts/totalContacts"] as number
    );

From Line 161 to 173, we employ the watchEffect hook so that the contacts/LOAD_CONTACTS action is dispatched immediately the component is created. Since the Vuex actions are promises, we have await them. The contacts/contactList action attaches a payload containing the nextPage and pageSize properties.

    const stopContactListEffect = watchEffect(async () => {
      await store
        .dispatch("contacts/LOAD_CONTACTS", {
          nextPage: nextPage.value,
          pageSize,
        })
        .then(() => {
          void nextTick(() => {
            tableRef.value?.refresh();
            loading.value = false;
          });
        });
    });

Also, the watchEffect hook is stored in the constant stopContactListEffect. At Line 230, we call the onBeforeUnmount hook with the stopContactListEffect as the handler. This removes the watchEffect from the component and prevent memory leaks. The watchEffect hook will automatically add variables used within it to its dependencies. If any of the values change, the watchEffect hook will re-run and lead to new contacts being fetched from the API server. By default, the watchEffect hook will run when the component is created in the setup hook.

When the contacts/LOAD_CONTACTS action is resolved, from Line 167, we call the nextTick hook and instruct the table to refresh (tableRef.value?.refresh();) and disable the loading state of the table (loading.value = false;). The nextTick hook is important so that the new value of the contacts/contactList getter is completely propagated and stored in the contacts computed ref at Line 153 before we refresh the table. Additionally, the contacts/LOAD_CONTACTS action will also update the totalContacts store property which will be propagated to the totalContacts computed ref at Line 157.

At Line 175, we define a constant lastPage, which stores the last page of all contacts we have based on the values of pageSize and totalContacts variables.

At Line 177, we modify the onScroll function which is attached to the QTable component as the handler of the virtual-scroll event (@virtual-scroll="onScroll"). See Line 28.

+    const onScroll = function ({ to, ref: ref2 }: VirtualScrollCtx): void {
      if (
        loading.value !== true &&
        nextPage.value < lastPage &&
-        to === lastIndex
+        to === nextPage.value * pageSize - 1
      ) {
+        tableRef.value = ref2;
        loading.value = true;
-
-        setTimeout(() => {
-          nextPage.value++;
-          void nextTick(() => {
-            ref2.refresh();
-            loading.value = false;
-          });
-        }, 500);
+        nextPage.value++;
      }
    };

The most important change above is the replacement of the setTimeout function (which was used to simulate an asynchronous operation) with nextPage.value++. When nextPage.value is incremented, it triggers the re-run of the watchEffect hook (because nextPage is a dependency in the watchEffect hook) at Line 161 which leads to the dispatch of the contacts/LOAD_CONTACTS action and population of the contacts table with new contacts for rendering.

At Line 217, we assign our contacts computed property to the rows property of the object returned from our setup hook.

At Line 243 and 244, we update thepagination property as shown below:

      pagination: {
        rowsPerPage: 0,
-        rowsNumber: rows.value.length,
+        page: nextPage.value,
+        rowsNumber: totalContacts.value,
      },

This concludes the changes in the ui/src/pages/Index.vue component.

Pointing the ui/src/pages/contacts/CreateContact.vue component at the Vuex Store

Open the ui/src/pages/contacts/CreateContact.vue file. Refer to this snapshot of the file. Copy-and-paste the content of the snapshot into the ui/src/pages/contacts/CreateContact.vue file. The template section remains on changed. So we focus on the script section.

At Line 56, we disable the no-misused-promises eslint rule. At Line 64, we import the nextTick hook from vue. At Line 70, we import the useStore hook from our store.

Within the setup hook, at Line 94, the useStore hook is called and used to initialise the store constant.

At Line 274, we replace the contact reactive object with the currentContact computed property which stores the value of the contacts/currentContact getter from our Vuex store.

At Line 283, within the watchEffect hook, we introduce the contacts/LOAD_CURRENT_CONTACT action which is immediately dispatched when the component is created. The action sends the contactId prop as the id of the contact being viewed or edited. Remember that the contactIdprop is automatically set byvue-routerwhen that route is visited. Then the action is resolved, we use thenextTick` hook to execute the iteration in the next process tick.

        await store
          .dispatch("contacts/LOAD_CURRENT_CONTACT", props.contactId)
          .then(() => {
            void nextTick(() => {
              Object.keys(currentContact.value).forEach((key) => {
                if (key !== "id") {
                  form[key].value = currentContact.value[key];
                }
              });
            });
          });

The iteration above (Lines 287 to 291) loops through the keys (omitting the id key) in the currentContact computed ref and assigns the value of the keys to the value property of each property in the form reactive object. This ensures that the fields on our form are populated with the values from the currentContact computed property.

This concludes the modifications in the ui/src/pages/contacts/CreateContact.vue component.

Pointing the ui/src/pages/contacts/ViewContact.vue component at the Vuex Store

Open the ui/src/pages/contacts/ViewContact.vue file. Refer to this snapshot of the file. Copy-and-paste the content of the snapshot into the ui/src/pages/contacts/ViewContact.vue file. The template section remains on changed. So we focus on the script section.

From Lines 201 to 210, we define the ContactData type:

type ContactData = Array<{
  icon: string;
  text: string | undefined | null | Array<string | null | undefined>;
  label: string;
  key: string;
  side?: string | undefined;
  sideColor?: string | undefined;
  clampLines?: number | "none";
  linkAs?: "email" | "tel" | "website";
}>;

At Line 205, we remove the reactive hook from vue as it is no longer need. We also remove the contacts import from Google_Contacts_Clone_Mock_Data.

At Line 223, we import the useStore hook from our Vuex store.

At Line 235, we instantiate the store reference and the currentContact computed ref

    const store = useStore();
    const currentContact = computed(
      // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
      () => store.getters["contacts/currentContact"] as Contact
    );

At Line 242, within the watchEffect hook, we call the contacts/LOAD_CURRENT_CONTACT action with the contactId prop as the id payload to load the current contact for the route.

At Line 247, we update the fullName computed property to concatenate the firstName and lastName from the currentContact computed property. Same for the jobDescription computed property at Lines 251 to 253.

From Line 256, we update the text property of each contactData object to read from the currentContact computed property.

At Line 333 to 339, we return currentCurrent, fullName, contactData, jobDescription, and isNullArray from the setup hook so that they are all available in the template section.

Save all files, serve the frontend, and test the functionalities on the contact tables, and view and edit a contact.

cd ui
yarn serve

This concludes this lesson. In the next lesson, we will connect the frontend of the Google Contacts Clone app to the API server.

Commit, merge with the master branch, and push to the remote repository (GitHub).

git add .
git commit -m "feat(api): complete connection of components to vuex store"
git push origin 17-connect-components-to-vuex-store
git checkout master
git merge master 17-connect-components-to-vuex-store
git push origin master