javascriptvue.jsnuxt.jsvue-routervue-props

Pass dynamic object data via nuxt-link to component


I have a project which is build in Nuxt 3 with a homepage which shows products. On that homepage, you can click on the title of the product and the product details page opens up. I archive this by using <NuxtLink>

<NuxtLink :to="{ name: 'slug', params: { slug: product.slug }}">
    <p>{{ product.title }}</p>
</NuxtLink>

When the user click on that link, the [slug].vue component will show the product details page. So far, so good. However, this leads to the [slug].vue component fetching the product data from the API again using its slug like this:

const { data } = await useFetch(runtimeConfig.public.api.product.view + `/${slug}`);

This behavior is also needed when the user visits the product details page directly from a link he saw on e.g. Facebook etc. But when the user clicks on the <NuxtLink> from the homepage, I would like to pass my product object to the [slug].vue component, as the homepage already has all needed information about the product the [slug].vue component needs to render.

If I will be able to pass my product object to the [slug].vue component, I could skip the extra API fetch and present the data right away to the user, increasing page speed and user experience.

I have already written the necessary code I need within my [slug].vue component to determine whether the props are empty and the component needs to fetch them from the API or the component can just use the props being passed and skip the fetch:

<script setup lang="ts">
  const { slug } = useRoute().params;
  const runtimeConfig = useRuntimeConfig()
  const route = useRoute()

  // Define component props
  const props = defineProps({
    product: null
  });

  // Computed property to determine whether data needs to be fetched
  const fetchData = computed(() => {
    return !props.product; // If props.data is not provided, fetch data
  });

  // Data ref to store fetched data
  const product = ref(null);

  // Fetch data from the API if fetchData is true
  if (fetchData.value) {
    const { data } = await useFetch(runtimeConfig.public.api.product.view + `/${slug}`);
    product.value = data.value;
  }
</script>

However, I am failing at passing my product object from my <NuxtLink> to the props of my [slug].vue component. How can I archive that?

I do not want to pass my product object from my homepage to my [slug].vue component using params. This will not work as the product object is quite long. I could stringify it with JSON.stringify() but I also don't want to pass data via my URL except the slug. Furthermore, the URL would also look very ugly, passing a very long JSON string to it.

I also do not want to use a storage like Pinia. I want to make use of the available props functionality, which was build to pass data between components.

If you have any other idea, how I can pass the data to my [slug].vue component from the homepage, let me know.

Kind regards


Solution

  • This used to be possible with params, but as of this change in vue router, it's not possible to do it without adding it as url query or slug, because this is considered as an anti-pattern.

    As noted in the link above, there are alternatives to achieve this, and personally i'd recommend to use the localStorage or vue store approach, but since you stated specifically that you don't want that, you can use history state as suggested in the same link above. A simple implementation would be adding state to the :to props of NuxtLink:

    // homepage /index
    <template>
      <div>
        <NuxtLink :to="{ name: 'slug', state: { product: toRaw(product) }}">
          <p>{{ product.title }}</p>
        </NuxtLink>
      </div>
    </template>
    
    <script setup>
    const product = ref({
      title: 'product title',
      slug: 'product123'
    })
    </script>
    

    Then:

    // product detail page /slug
    <template>
      <section>
        <h1>{{ product?.title }}</h1>
      </section>
    </template>
    
    <script setup>
    const product = ref(null)
    
    onMounted(() => {
      product.value = history.state.product // get product from previous route
    
      if (!product.value) {
        // fetch product from API if it doesn't exist
      }
    })
    </script>
    

    We need to pass a non-reactive object to state, so we can use toRaw like the above code to convert the reactive state to normal object. Then on the product detail page, you can access the data with

    history.state.product
    

    This approach, however, will persist the state when the page is reloaded. If you want to clear the state when you reload, you can add these additional codes at the end of the onMounted of the product detail page:

    // product detail page /slug
    onMounted(() => {
      product.value = history.state.product // get product from previous route
    
      if (!product.value) {
        // fetch product from API if it doesn't exist
      }
    
      const { product: _product, ...currentState } = history.state // get only other keys than product key
      window.history.replaceState({ ...currentState }, '') // remove state
    })
    

    I will say again that it's better to use other approach than this as suggested by the vue router change.