Search code examples
vue.jsvuejs3vue-routerpinia

how to properly fetch a single product in from my api in Vue3


I'm using Pinia to store my catalog of products which it gets from my API (Django), this gets displayed in a table. I want to be able to click on one of the products and see detailed info about that product.

Should I be making a new API call for this info or should I have it stored in Pinia?

Also, how do I access the id of the product from the URL?

this.$route.params.id 

I tried this ^^^ and I keep getting

$route is undefined

this is my Pinia store

import { defineStore } from "pinia";
import axios from "axios";

export const useProductsStore = defineStore("products", {
  state: () => {
    return {
      products: [],
      vendors: [],
      vendorPrices: [],
      productDetail: [],
    };
  },

  getters: {
    getProducts: (state) => {
      return state.products;
    },
    getProductDetails: (state) => {
      return (id) => state.products.find((product) => product.id === id);
    },
  },

  actions: {
    async fetchProducts() {
      try {
        const data = await axios.get("http://127.0.0.1:8000/products.json");
        this.products = data.data;
      } catch (error) {
        alert(error);
        console.log(error);
      }
    },
  },
});

this is my table component that shows the whole catalog (this works fine)

<template>
  <div class="card">
    <DataTable :value="products" dataKey="id" responsiveLayout="scroll" v-model:selection="selectedProducts"
      :paginator="true" :rows="16"
      paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
      :rowsPerPageOptions="[16, 50, 100]" currentPageReportTemplate="Showing {first} to {last} of {totalRecords}"
      scrollHeight="75vh" :resizableColumns="true" columnResizeMode="fit" showGridlines stripedRows stickyHeader="true">


      <Column selectionMode="multiple" headerStyle="width: 3em"></Column>

      <Column field="sku" header="SKU" style="width:10%" :sortable="true">
        <template #body="slotProps">
          <router-link :to="'/catalog/' + slotProps.data.id">{{ slotProps.data.sku }}</router-link>
        </template>
      </Column>

      <Column field="name" header="Name" style="width:30%" :sortable="true"></Column>

      <Column field="description" header="Description" style="width:30%"></Column>

      <Column field="asin" header="ASIN" style="width:30%" :sortable="true"></Column>

      <!-- <Column field="vendors" header="Vendors" style="width:30%" :sortable="true"></Column> -->

      <Column field="brand" header="Brand" style="width:20%" :sortable="true"></Column>

      <Column field="price" header="Price" style="width:20%" :sortable="true"></Column>
    </DataTable>
  </div>

</template>

<script setup>
import { ref, onMounted, computed } from "vue";
import { useProductsStore } from "../stores/products";

const store = useProductsStore();

const getProducts = computed(() => {
  return store.getProducts;
});

const products = computed(() => {
  return store.products;
});

onMounted(() => {
  store.fetchProducts()
})
</script>

You can see that I made the "sku" into a router link that leads to a page that's supposed to show the product details.

here is the component displaying the product details

<template>
  <div>
    <h1>Product Details</h1>
    <h1>{{ productDetails.name }}</h1>
    <h2>{{ productDetails.sku }}</h2>
    <h3>{{ productDetails.asin }}</h3>
    <!-- <h3>{{ productDetails.price }}</h3> -->
    <h3>{{ productDetails.description }}</h3>

  </div>
</template>  

<script setup>
import { ref, onMounted, computed } from "vue";
import { useProductsStore } from "../stores/products";

const store = useProductsStore();

const products = computed(() => {
  return store.products;
});

const getProductDetails = computed(() => {
  return store.getProductDetails(ProductId);
});

const productDetails = computed(() => {
  return products;
});
</script>

and here is my router

import { createWebHistory, createRouter } from "vue-router";
import Dashboard from "../views/Dashboard.vue";
import Vendors from "../views/vendorPage.vue";
import Catalog from "../views/catalogTable.vue";
import addProduct from "../views/addProduct.vue";
import productDetail from "../views/productDetail.vue";

const routes = [
  { path: "/", name: "Dashboard", component: Dashboard },
  { path: "/vendors", name: "Vendors", component: Vendors },
  { path: "/catalog", name: "Catalog", component: Catalog },
  {
    path: "/catalog/:id",
    name: "productDetail",
    component: productDetail,
    params: true,
  },
  { path: "/add", name: "AddProduct", component: addProduct },
];

const router = createRouter({
  history: createWebHistory(),
  routes,
});

export default router;

Solution

  • Since you're using Composition API, you need to import and use the composable rather than this.$route.params.id.

    This is how

    <script setup>
    import { useRoute } from 'vue-router'
    const route = useRoute()
    
    console.log('current param id', route.params.id)
    </script>
    

    as explained here.


    Otherwise, since you have a /catalog/:id route, you should probably have a page where you fetch a specific one indeed.
    Deciding if it should be in Pinia or fetched again comes down to the specificities of your app, how you want to handle your state, and other concerns. (also quite an opinion-based question)

    I'd say, keep it easy initially and iterate on it afterward if some improvement is needed.

    PS: I've noticed you're fetching it from a JSON, I guess it's for testing purposes and that you have a real API with your Django backend.