w3bdesign/nuxtjs-woocommerce

Synchronize remote cart with local cart

w3bdesign opened this issue · 2 comments

We can show the Pinia cart this way and remove the window.location.reload() but first we need to synchronize the WooCommerce cart with the Pinia state cart.

<template>
  <div v-if="remoteError">
    <span class="text-xl text-red-500"
      >Error fetching cart. Please refresh the page.</span
    >
  </div>
  <div v-else class="mt-4 lg:mt-0">
    Cart count from state: {{ cart.getCartQuantity }}<br />
    Cart total from state: {{ cart.getCartTotal }}

    <transition name="cart">
      <span
        v-if="cart.getCartQuantity"
        class="text-xl text-white no-underline lg:text-black is-active"
      >
        <span class="hidden lg:block">
          <nuxt-img
            alt="Cart icon"
            class="h-12 ml-4 lg:ml-2"
            aria-label="Cart"
            src="/svg/Cart.svg"
          />
        </span>
        <span class="block lg:hidden">
          <Icon
            name="ri:shopping-cart-2-line"
            size="3em"
            class="text-white lg:text-black"
          />
        </span>
      </span>
    </transition>
    <transition name="cart">
      <div v-if="cart.getCartQuantity">
        <span
          class="absolute w-6 h-6 pb-2 ml-16 -mt-12 text-center text-black bg-white lg:text-white lg:bg-black rounded-full lg:ml-14"
        >
          {{ cart.getCartQuantity }}
        </span>
        <span class="text-white lg:text-black">
          Total: {{ cart.getCartTotal }}
        </span>
      </div>
    </transition>

    <NuxtLink to="/cart">
      <transition name="cart">
        <span
          v-if="cartLength && !loading"
          class="text-xl text-white no-underline lg:text-black is-active"
        >
          <span class="hidden lg:block">
            <nuxt-img
              alt="Cart icon"
              class="h-12 ml-4 lg:ml-2"
              aria-label="Cart"
              src="/svg/Cart.svg"
            />
          </span>
          <span class="block lg:hidden">
            <Icon
              name="ri:shopping-cart-2-line"
              size="3em"
              class="text-white lg:text-black"
            />
          </span>
        </span>
      </transition>
      <transition name="cart">
        <div v-if="cartLength && !loading">
          <span
            class="absolute w-6 h-6 pb-2 ml-16 -mt-12 text-center text-black bg-white lg:text-white lg:bg-black rounded-full lg:ml-14"
          >
            {{ cartLength }}
          </span>
          <span class="text-white lg:text-black">
            Total: {{ formatPrice(`${subTotal}`) }}
          </span>
        </div>
      </transition>
    </NuxtLink>
  </div>
</template>

<script setup>
import { ref, onBeforeMount, computed, watch } from "vue";

import GET_CART_QUERY from "@/apollo/queries/GET_CART_QUERY.gql";
import { getCookie } from "@/utils/functions";
import { useCart } from "@/store/useCart";

const cart = useCart();

const cartChanged = ref(false);
const loading = ref(true);
const remoteError = ref(false);

const { data, error, pending, execute } = useAsyncQuery(GET_CART_QUERY, {
  fetchPolicy: "cache-and-network",
});

const cartContents = computed(() => data.value?.cart?.contents?.nodes || []);
const cartLength = computed(() =>
  cartContents.value.reduce((total, product) => total + product.quantity, 0)
);
const subTotal = computed(() =>
  cartContents.value.reduce(
    (total, product) => total + Number(product.total.replace(/[^0-9.-]+/g, "")),
    0
  )
);

// Watch for changes in the cart quantity and set a flag if it changes
watch(cartLength, (newLength, oldLength) => {
  if (newLength !== oldLength) {
    cartChanged.value = true;
  }
});

// Execute the query initially
onBeforeMount(() => {
  execute();
});

// When the component is mounted, stop the loading state
loading.value = false;

// Watch for the cartChanged flag and execute the query when it changes
watch(cartChanged, (newValue) => {
  if (
    newValue &&
    process.client &&
    !pending.value &&
    getCookie("woo-session")
  ) {
    execute();
    cartChanged.value = false; // Reset the flag after executing the query
  }
});

// Watch for errors from the Apollo query
watch(error, (newError) => {
  remoteError.value = !!newError;
});
</script>

To synchronize the local cart state with the remote cart without using manual polling like setInterval, you can leverage the reactive capabilities of Vue and Pinia, along with the Apollo Client's built-in features. Here's how you can approach this:

  1. Apollo Client Cache Subscription: Apollo Client allows you to subscribe to changes in the cache. You can use this feature to reactively update the local cart state whenever the remote cart changes.

  2. GraphQL Subscriptions: If your backend supports GraphQL subscriptions, you can use them to listen to real-time changes to the cart on the server. This would push updates to the client without the need for polling.

  3. WebSockets: If GraphQL subscriptions are not an option, you can use WebSockets to listen for changes on the server and update the local state accordingly.

  4. Built-in Apollo Client Reactivity: Apollo Client's watchQuery or subscribeToMore can be used to automatically refetch queries when certain conditions are met or when mutations occur.

Here's an example of how you might use Apollo Client's cache subscription to keep the local cart state in sync:

import { watchEffect } from 'vue';
import { useApolloClient } from '@vue/apollo-composable';

// Inside your setup function
const apolloClient = useApolloClient();

watchEffect(() => {
  // This will run whenever the Apollo cache data for this query changes
  const cacheWatcher = apolloClient.watchQuery({
    query: GET_CART_QUERY,
    fetchPolicy: 'cache-and-network',
  }).subscribe({
    next(data) {
      // Update local cart state with the new data from the cache
      cart.setCartItems(data.cart.contents.nodes);
    },
    error(err) {
      console.error('Error syncing cart:', err);
    },
  });

  // Cleanup the subscription when the component is unmounted
  onBeforeUnmount(() => {
    cacheWatcher.unsubscribe();
  });
});

In the above code, cart.setCartItems would be a new action in your Pinia store that updates the cart items based on the data from the Apollo cache.

For the useCart store, you would add this action:

actions: {
  // ... other actions
  setCartItems(items) {
    this.cart = items.map(item => ({
      // Map the item to the structure expected by your local cart state
      slug: item.product.node.slug,
      quantity: item.quantity,
      price: item.total,
      // ... any other properties you need
    }));
  },
  // ... other actions
}

Remember to handle the cleanup of subscriptions to prevent memory leaks. The onBeforeUnmount lifecycle hook is used in the example to unsubscribe when the component is destroyed.

This approach ensures that your local cart state is reactively updated whenever the remote cart changes, without the need for manual polling.

Subscribing Side Effects on Store Changes One significant advantage of Pinia is the ability to extend the store’s functionalities and implement side effects using plugins. With this ability, we can easily subscribe to changes in all the stores or in a specific store to perform additional actions like synchronizing data with the server when needed. Take the following cartPlugin, for instance: //main.ts import { cartPlugin } from '@/plugins/cartPlugin' //... const pinia = createPinia() pinia.use(cartPlugin) app.use(pinia) //... The cartPlugin is a function that receives an object containing a reference to the app instance, the pinia instance, the store instance, and an options object. Vue will trigger this function once for every store in our application. To make sure we are subscribing only to the cart store, we can check the store’s id (see Example 9-17). Example 9-17. Cart plugin //src/plugins/cartPlugin.ts export const cartPlugin = ({ store}) => { if (store.$id === 'cart') { //... } } Then we can subscribe to the cart store changes using the store.$subscribe method, as in Example 9-18. Example 9-18. Cart plugin subscribing to store changes //src/plugins/cartPlugin.ts export const cartPlugin = ({ store}) => { if (store.$id === 'cart') { store.$subscribe((options) => { console.log('cart changed', options) }) } } 236 | Chapter 9: State Management with Pinia