skip to Main Content

I have been working on an SPA with Vue 3, TypeScript and The Movie Database (TMDB) API.

I want to display the movie poster on the movie details page if there is a poster, otherwise I want to display a placeholder (generic-poster-big.png).

For this purpose, in srccomponentsMovieDetails.vue, I have:

<template>
  <div class="poster-container text-center text-sm-start my-3">
    <img
      :src="genericPoster"
      :alt="movie?.title"
      class="img-fluid shadow-sm"
    />
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import { useRoute } from "vue-router";
import axios from "axios";
import env from "../env";
import { Movie } from "../models/Movie";

export default defineComponent({
  name: "MovieDetails",

  data() {
    return {
      route: useRoute(),
      genericPosterBig: require("../assets/generic-poster-big.png"),
      movie: null as Movie | null
    };
  },

  mounted() {
    this.getMovieDetails();
  },

  methods: {
    getMovieDetails() {
      axios
        .get(
          `${env.api_url}/movie/${this.route.params.id}?api_key=${env.api_key}`
        )
        .then((response) => {
          this.movie = response.data;
        })
        .catch((err) => console.log(err));
    },

  computed: {
    genericPoster() {
      return !this.movie?.poster_path
        ? this.genericPosterBig
        : `https://image.tmdb.org/t/p/w500/${this.movie?.poster_path}`;
    },
  }
});
</script>

The problem

When there is a movie poster, before it loads, the generic poster appears, for a fraction of a second, yet visible.

Stackblitz

You can see a Stackblitz HERE.

Questions

  1. What am I doing wrong?
  2. What is the most reliable way to fix this problem?

3

Answers


  1. Chosen as BEST ANSWER

    Apparently, making sure that the movie is loaded before trying to render the poster at all, with v-if, fixed the issue:

     <img v-if="movie"
          :src="genericPoster"
          :alt="movie?.title"
          class="img-fluid shadow-sm"
     />
    

  2. Something like the following should do the trick, added lines marked with **********

    <template>
      <div class="poster-container text-center text-sm-start my-3">
        <!-- ************ -->
        <img v-if="loaded"
          :src="genericPoster"
          :alt="movie?.title"
          class="img-fluid shadow-sm"
        />
      </div>
    </template>
    
    <script lang="ts">
    import { defineComponent } from "vue";
    import { useRoute } from "vue-router";
    import axios from "axios";
    import env from "../env";
    import { Movie } from "../models/Movie";
    
    export default defineComponent({
      name: "MovieDetails",
    
      data() {
        return {
          //************************
          loaded: false, 
          route: useRoute(),
          genericPosterBig: require("../assets/generic-poster-big.png"),
          movie: null as Movie | null
        };
      },
    
      mounted() {
        this.getMovieDetails();
      },
    
      methods: {
        getMovieDetails() {
          axios
            .get(
              `${env.api_url}/movie/${this.route.params.id}?api_key=${env.api_key}`
            )
            .then((response) => {
              this.movie = response.data;
            })
            .catch((err) => console.log(err))
            //****************
            .finally(() => this.loaded = true); 
        },
    
      computed: {
        genericPoster() {
          return !this.movie?.poster_path
            ? this.genericPosterBig
            : `https://image.tmdb.org/t/p/w500/${this.movie?.poster_path}`;
        },
      }
    });
    </script>
    
    Login or Signup to reply.
  3. This behaviour is expected given your solution, but I understand you want to improve this for UX reasons.

    The problem is that your component is mounted before the data is available, so you need to set a logic on what to do while it’s loading

    I see two solutions for this:

    1. Set your placeholder image only in case the main image fails to load (i.e. when the url is wrong or it’s empty). So it will only loads your placeholder after trying to load the main image, not before.

    While it loads, use a loading spinner on your image while it’s loading, not the image placeholder (which should appear only once the poster details has been fetched). This is useful to avoid layout flickering: it will reserve the space required by the image.

    <template>
      <div class="poster-container text-center text-sm-start my-3">
        <div v-if="loading">Loading spinner...</div>
        <img
          v-else
          :src="movie.poster_path"
          :alt="movie.title"
          class="img-fluid shadow-sm"
          @error="setPlaceholder"
        />
      </div>
    </template>
    
    <script>
    {
      data() {
        return {
           movie: null,
           loading: true,
           genericPosterBig: null,
        }
      }
      mounted() {
        this.getMovieDetails()
      },
      methods: {
        getMovieDetails() {
          this.loading = true
          axios.get(`${env.api_url}/movie/${this.route.params.id}?api_key=${env.api_key}`)
            .then(({ data }) => {
              this.movie = data;
            })
            .catch((err) => console.log(err))
            .finally(() => {
              this.loading = false
            });
        },
        setPlaceholder(event) {
           // This loads the image only if it's not already assigned
           this.genericPosterBig ||= require("../assets/generic-poster-big.png")
           event.target.src = this.genericPosterBig
        }
      }
    }
    </script>
    
    1. Using the Nuxt framework, you could preload the movies details before mounting the movie details page using the useAsyncData() composable (or for option api), so you are sure you already have all the movies data when creating the details page (so no flickering).
    {
      // This will block the page rendering until the promise is settled.
      async asyncData() {
         const movieDetails = await axios
            .get(`${env.api_url}/movie/${this.route.params.id}?api_key=${env.api_key}`)
            .then((response) => response.data)
         return { movieDetails }
      }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search