Search code examples
vue.jspinia

Why the slice method doesn't work on array prop?


I have the following code which defines a Pinia storage:

import { ref, computed, shallowRef } from 'vue'
import { defineStore } from 'pinia'

export const usePokemonStore = defineStore('pokemons', () => {
    // define the pokemons list state
    const pokemonsList = ref([]);
    const pokemonsLoaded = ref([]);
    const pokemonsLoadedNames = ref([]);

    // computed
    const pokemonsListLength = computed(() => pokemonsList.value.length)
    const pokemonsLoadedLength = computed(() => pokemonsLoaded.value.length)

    // actions
    async function getList() {
        const res = await fetch('https://pokeapi.co/api/v2/pokemon?limit=100000&offset=0');
        const data = await res.json();

        pokemonsList.value = data["results"];
    }

    async function loadPokemon(name) {
        const URI = `https://pokeapi.co/api/v2/pokemon/${name}`

        const res = await fetch(URI);
        const data = await res.json();

        pokemonsLoadedNames.value.push(data["name"])
        pokemonsLoaded.value.push(data)
    }

    async function loadPokemons(offset, limit){
        // basic check for limits
        limit = limit > pokemonsListLength ? pokemonsListLength : limit;
        limit = limit < 0 ? 10 : limit

        // basic check for offset
        offset = offset < 0 ? 0 : offset;
        offset = offset > pokemonsListLength ? 0 : offset

        for (let i = offset; i < offset+limit; i++){

            // if the pokemon is already loaded skips the request for it
            if (pokemonsLoadedNames.value.includes(pokemonsList.value[i].name)) {
                continue;
            }

            // requests the pokemon given a name
            loadPokemon(pokemonsList.value[i].name)
        }
    }

    return {
        pokemonsList, 
        pokemonsLoaded, 
        pokemonsListLength, 
        
        pokemonsLoadedLength, 
        pokemonsLoadedNames, 
        
        getList,
        loadPokemon,
        loadPokemons
    }
})

And I have the following component which makes use of that storage to get the pokemons:

<template>
    <div class="pokedex">
        <PokemonImage class="pokemon-figure" pokemon="" />
        <ul v-if="pokemonsToShow" class="pokemon-showcase">
            <li class="pokemon-item" v-for="pokemon in pokemonsToShow">
                <PokemonCard :pokemon="pokemon" />
            </li>
        </ul>
        <div class="navigation">
            <button v-show="page !== 1" @click="pageChange(-1)">Previous Page</button>
            <button @click="pageChange(1)">Next Page</button>
        </div>
        {{ page }}
    </div>
</template>

<script setup>
import { onBeforeMount, ref, computed, watch } from 'vue';
import { usePokemonStore } from '../stores/pokemon'
import PokemonCard from '../components/PokemonCard.vue'
import PokemonImage from '../components/PokemonImage.vue'
const pokeStore = usePokemonStore();

const page = ref(1)

const pokemonsToShow = ref([])

// offset and limit calculate based on the page
const limit = computed(() => 20 );
const offset = computed(() => page.value * limit.value - limit.value);

// initial load
onBeforeMount(async () => {
    await pokeStore.getList()
    await pokeStore.loadPokemons(0, limit.value)
    pokemonsToShow.value = pokeStore.pokemonsLoaded.slice(0, pokeStore.pokemonsLoadedLength)
})

const pageChange = async (step) => {
    page.value = page.value + step
    await pokeStore.loadPokemons(offset.value, limit.value)

    const start = offset.value;
    const end = offset.value + limit.value;
    console.log(start, end)
    console.log(pokeStore.pokemonsLoaded)
    pokemonsToShow.value = pokeStore.pokemonsLoaded.slice(start, end)
    console.log(pokemonsToShow.value)
}
</script>

Now when the user clicks on the page button the page.value is updated so that the computed values for the offset and the limit are also updated (in reality only the offset updates) that way if the page is new I can load new pokemons from that which I do by calling the pokeStore.loadPokemons(offset.value, limit.value) function and awaiting for that inside the pageChange function. But now I want to change the pokemonsToShow so I want to get a slice of the array of loaded pokemons in the storage but every time I try to slice that array I get back nothing, even though when I print the array using console.log(pokeStore.pokemonsLoaded) the array shows as updated with the new values, and the ranges are correct.

I'm expecting the array to slice correctly since if I put static values in this function call:

    pokemonsToShow.value = pokeStore.pokemonsLoaded.slice(2, 4)
}

It works for some reason, but not with the values calculated dinamically


Solution

  • This is a tricky thing about console.log(). console.log(pokeStore.pokemonsLoaded) will show you the result of the fetched data even if console.log is in reality executed before the fetch is done. This is due to the fact that many browsers show a "live" view of object data.

    https://developer.mozilla.org/en-US/docs/Web/API/Console/log#logging_objects

    Don't use console.log(obj), use console.log(JSON.parse(JSON.stringify(obj))) ... many browsers provide a live view that constantly updates as values change. This may not be what you want.

    It is probable then that the array has not actually been updated at the time you slice it. I also believe this is true because even though you await this call: await pokeStore.loadPokemons(...), that function does not await it's call to loadPokemon(). Since there is no await, the function immediately finishes executing before the fetch has finished and returns to your component code.

    I believe if you do await that call, everything should start working

    async function loadPokemons(offset, limit){
      .
      .
      .
      await loadPokemon(pokemonsList.value[i].name)
    }