Search code examples
vue.jsvue-componentvuejs3vue-composition-apipinia

How to set up Pinia getter in Vue 3 Composition API


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>


Solution

  • 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>