How to set up Pinia getter in Vue 3 Composition API
Asked Answered
B

1

3

I am building a Pokemon filtered search app using Vue 3, Composition API, and Pinia. I am attempting to set up the app so that the fetched response from the Pokemon API is passed to a store (set up using Pinia) inside the fetchPokemon() function.

    const fetchPokemon = () => {
      axios.get("https://pokeapi.co/api/v2/pokemon?offset=0")
      .then((response) => {
        store.addPokemon(response.data.results)
      })
    }

After passing the response to the store, the updatePokemon() function uses filter and include methods to filter out and match Pokemon in the store with Pokemon in the user-input text field ("state.text"):

    const updatePokemon = () => {
      if(!state.text) {
        return []
      }
      return store.getState.pokemons.filter((pokemon) => 
        pokemon.name.includes(state.text)
      )
    }

When executing the app, I am getting the following error in the updatePokemon() function:

Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'includes')

I'm assuming this means the .includes() method for searching/filter cannot be used for this search. How should I go about handling the filter and include methods to match Pokemon in the store with the user-inputted Pokemon?

Here is the code:

Pinia Store

import { defineStore } from 'pinia'

export const usePokemonStore = defineStore({
    id: 'store',
    state: () => ({
        pokemons: []
    }),
    getters: {
        getState(state) {
            return state
        }
    },
    actions: {
        addPokemon(name) {
            this.pokemons.push(name)
        }
    }
})

Component

<template>
  <div class="w-full flex justify-center">
    <input type="text" placeholder="Enter Pokemon here" 
    class="mt-10 p-2 border-blue-500 border-2" v-model="text"/>
  </div>
  <div class="mt-10 p-4 flex flex-wrap justify-center">
    <div class="ml-4 text-2x text-blue-400" 
    v-for="(pokemon, idx) in filteredPokemon" :key="idx">
      <router-link :to="`/about/${getPokemonId(pokemon.name)}`">
        {{ pokemon.name }} - with id {{ getPokemonId(pokemon.name) }}
      </router-link>
    </div>
  </div>
</template>

<script>
import axios from 'axios';
import { reactive, toRefs, computed } from 'vue';
import { usePokemonStore } from '@/store';

export default {
  name: 'Home',
  setup() {

    const store = usePokemonStore()

    const state = reactive({
      text: "",
      filteredPokemon: computed(()=> updatePokemon())
    })

    const updatePokemon = () => {
      if(!state.text) {
        return []
      }
      return store.getState.pokemons.filter((pokemon) => 
        pokemon.name.includes(state.text)
      )
    }

    const fetchPokemon = () => {
      axios.get("https://pokeapi.co/api/v2/pokemon?offset=0")
      .then((response) => {
        store.addPokemon(response.data.results)
      })
    }

    fetchPokemon()

    const getPokemonId = (item) => {
      console.log(item)
      return store.pokemons.findIndex((p) => p.name === item) + 1
    }

    return { ...toRefs(state), fetchPokemon, getPokemonId, updatePokemon, store }
  }
}
</script>

UPDATED

Store - with not action

import { defineStore } from 'pinia'

export const usePokemonStore = defineStore({
    id: 'store',
    state: () => ({
        pokemons: []
    })
})

Component - with no store.addPokemon(...)

<template>
  <div class="w-full flex justify-center">
    <input type="text" placeholder="Enter Pokemon here" 
    class="mt-10 p-2 border-blue-500 border-2" v-model="text"/>
  </div>
  <div class="mt-10 p-4 flex flex-wrap justify-center">
    <div class="ml-4 text-2x text-blue-400" 
    v-for="(pokemon, idx) in filteredPokemon" :key="idx">
      <router-link :to="`/about/${getPokemonId(pokemon.name)}`">
        {{ pokemon.name }} - with id {{ getPokemonId(pokemon.name) }}
      </router-link>
    </div>
  </div>
</template>

<script>
import axios from 'axios';
import { reactive, toRefs, computed } from 'vue';
import { usePokemonStore } from '@/store';

export default {
  name: 'Home',

  setup() {

    const store = usePokemonStore()

    const state = reactive({
      // pokemons: [],
      text: "",
      filteredPokemon: computed(()=> updatePokemon())
    })

    const updatePokemon = () => {
      if(!state.text) {
        return []
      }
      return store.pokemons.filter((pokemon) => 
        pokemon.name.includes(state.text)
      )
    }

    const fetchPokemon = () => {
      axios.get("https://pokeapi.co/api/v2/pokemon?offset=0")
      .then((response) => {
        store.pokemons = response.data.results
      })
    }

    fetchPokemon()

    const getPokemonId = (item) => {
      console.log(item)
      return store.pokemons.findIndex((p) => p.name === item) + 1
    }

    return { ...toRefs(state), fetchPokemon, getPokemonId, store }
  }
}
</script>

Boneyard answered 12/3, 2022 at 18:22 Comment(0)
A
5

First of all, you don't need getState at all.

You can use usePokemonStore().pokemons directly. The object returned by calling usePokemonStore() function includes:

  • all state properties
  • all actions
  • all getters.

Here's how to get the filtered pokemon array, based on whether their name includes state.text:

setup() {
  const store = usePokemonStore();
  const state = reactive({
    text: "",
    filteredPokemons: computed(() => store.pokemons.filter(
      pokemon => pokemon.name.includes(state.text)
    ))
  });
  return {
    ...toRefs(state)
  }
}

Working example:

const { createApp, reactive, toRefs, computed, onMounted } = Vue;
const { defineStore, createPinia } = Pinia;

const usePokemons = defineStore('pokemon', {
  state: () => ({ pokemons: [] })
});
const pinia = createPinia();
createApp({
  pinia,
  setup() {
    const store = usePokemons(pinia);
    const state = reactive({
      searchTerm: '',
      filteredPokemons: computed(() => store.pokemons.filter(
        pokemon => pokemon.name.includes(state.searchTerm)
      ))
    });
    onMounted(() => {
      fetch('https://pokeapi.co/api/v2/pokemon?offset=0')
        .then(r => r.json())
        .then(r => store.pokemons = r.results)
    });
    return {
      ...toRefs(state)
    }
  }
}).mount('#app')
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://unpkg.com/vue-demi"></script>
<script src="https://unpkg.com/[email protected]/dist/pinia.iife.prod.js"></script>
<div id="app">
  <input v-model="searchTerm">
  <div v-for="pokemon in filteredPokemons">
    {{ pokemon.name }}
  </div>
</div>
Antechoir answered 12/3, 2022 at 18:41 Comment(7)
Btw storeToRefs is a more convenient alternative to toRefsNonmetallic
@Antechoir thanks. I removed getState and and then relocated the filter functionality directly to filteredPokemon in the reactive state, just like your snippet shows. However, I'm still getting the same error message about the .includes() method: Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'includes')Boneyard
I've added a working demo to the answer. If that doesn't help, please create a minimal reproducible example either here or on codesandbox.io (or similar), making sure it reproduces the bug you're having.Antechoir
@JS_is, make sure the pokemons on the state is always an array. Initially, an empty array. And after you get the result, you assign the result.Antechoir
@Antechoir thanks! I removed the action in the store, as well as the addPokemons handler. I replaced addPokemons with store.pokemons = response.data.results, which I believe is logically similar to store.pokemons = r.results in your snippet. This works. However, why is it necessary to pass the response results directly to store instead of pushing the response to the store with an action handler? In other words, why didn't it work to use an action handler to pass the response to store?Boneyard
@JS_is, your addPokemon function adds strings to an array which is supposed to contain objects. Another thing: if your method sets the pokemons you should name it setPokemons not addPokemons. An addPokemons would look like this: addPokemons(pokemons) { this.pokemons.push(...pokemons) }. A setPokemons would look like this: setPokemons(pokemons) { this.pokemons = pokemons }. In both cases, the pokemons would be {name: string; url: string }[].Antechoir
To answer your question "why is it necessary to pass the response directly to the store". It's not! You don't have to assign vs push. Assign replaces the entire array whereas push adds new items. Both work. But you need to make sure all members of the array have the same structure, at all times.Antechoir

© 2022 - 2024 — McMap. All rights reserved.