Skip to main content
The Favorite Store provides a global state management solution for tracking user’s favorite Pokemon, with automatic persistence to localStorage and optimized selector hooks.

Store Definition

import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import { PokemonSummary } from '@/types'

interface Store {
  favorites: PokemonSummary[]
  toggleFavorite: (pokemon: PokemonSummary) => void
  isFavorite: (id: number) => boolean
}

const useFavoriteStore = create<Store>()(persist(...))

State

favorites

favorites
PokemonSummary[]
Array of favorited Pokemon, each containing:
  • id (number): Pokemon ID
  • name (string): Pokemon name
  • types (string[]): Array of type names
  • image (string): Sprite image URL

Actions

toggleFavorite

Adds or removes a Pokemon from favorites.
toggleFavorite(pokemon: PokemonSummary): void
pokemon
PokemonSummary
required
Pokemon object to toggle. Must contain id, name, types, and image.
Behavior:
  • If Pokemon is already favorited → Removes it
  • If Pokemon is not favorited → Adds it
  • Automatically saves to localStorage

isFavorite

Checks if a Pokemon is in favorites.
isFavorite(id: number): boolean
id
number
required
Pokemon ID to check
Returns: true if Pokemon is favorited, false otherwise

Usage

Basic Toggle

import { useFavoriteStore } from '@/stores/favorite.store'

function FavoriteButton({ pokemon }) {
  const toggleFavorite = useFavoriteStore(state => state.toggleFavorite)
  const isFavorite = useFavoriteStore(state => state.isFavorite(pokemon.id))

  return (
    <button onClick={() => toggleFavorite(pokemon)}>
      {isFavorite ? '♥ Remove' : '♡ Add'}
    </button>
  )
}

Display Favorites List

import { useFavoriteStore } from '@/stores/favorite.store'

function FavoritesList() {
  const favorites = useFavoriteStore(state => state.favorites)

  if (favorites.length === 0) {
    return <p>No favorites yet!</p>
  }

  return (
    <div className="grid">
      {favorites.map(pokemon => (
        <PokemonCard key={pokemon.id} {...pokemon} />
      ))}
    </div>
  )
}

Optimized Selector Hooks

The store provides specialized hooks for common use cases:

useFavoriteState

Returns the full favorites array.
useFavoriteState(): PokemonSummary[]
Usage:
import { useFavoriteState } from '@/stores/favorite.store'

function FavoriteCount() {
  const favorites = useFavoriteState()
  
  return <span>{favorites.length} favorites</span>
}

useIsFavorite

Optimized hook to check if a specific Pokemon is favorited.
useIsFavorite(id: number): boolean
id
number
required
Pokemon ID to check
Usage:
import { useIsFavorite } from '@/stores/favorite.store'

function PokemonCard({ pokemon }) {
  const isFavorite = useIsFavorite(pokemon.id)

  return (
    <div className={isFavorite ? 'favorited' : ''}>
      <h3>{pokemon.name}</h3>
      {isFavorite && <span>♥</span>}
    </div>
  )
}
Performance: Only re-renders when the favorite status of this specific Pokemon changes, not when any favorite changes.

useFavoriteActions

Returns action functions without subscribing to state changes.
useFavoriteActions(): { toggleFavorite: (pokemon: PokemonSummary) => void }
Usage:
import { useFavoriteActions } from '@/stores/favorite.store'

function AddToFavoritesButton({ pokemon }) {
  const { toggleFavorite } = useFavoriteActions()

  return (
    <button onClick={() => toggleFavorite(pokemon)}>
      Add to Favorites
    </button>
  )
}
Performance: Component never re-renders due to favorites changing (actions are stable references).

Implementation Details

Toggle Logic

toggleFavorite: (pokemon) => {
  const { favorites, isFavorite } = get()
  
  // Create clean copy of pokemon data
  const newPokemon: PokemonSummary = {
    id: pokemon.id,
    name: pokemon.name,
    types: pokemon.types,
    image: pokemon.image,
  }
  
  set({
    favorites: isFavorite(newPokemon.id)
      ? favorites.filter(p => p.id !== newPokemon.id)  // Remove
      : [...favorites, newPokemon]                      // Add
  })
}
Key points:
  • Creates a clean copy to ensure only necessary data is stored
  • Uses functional update pattern for immutability
  • Automatically triggers localStorage persistence

Persistence Configuration

persist(
  // ... store implementation
  {
    name: 'POKENEX-FAVORITE-LIST',
  }
)
Storage:
  • Key: POKENEX-FAVORITE-LIST
  • Location: localStorage
  • Format: JSON stringified array
  • Persistence: Survives page refreshes and browser restarts

Complete Examples

Favorite Toggle with Icon

import { useFavoriteStore, useIsFavorite } from '@/stores/favorite.store'
import { Heart } from 'lucide-react'

function FavoriteIcon({ pokemon }) {
  const isFavorite = useIsFavorite(pokemon.id)
  const { toggleFavorite } = useFavoriteStore(state => ({
    toggleFavorite: state.toggleFavorite
  }))

  return (
    <button
      onClick={() => toggleFavorite(pokemon)}
      className={`favorite-btn ${isFavorite ? 'active' : ''}`}
      aria-label={isFavorite ? 'Remove from favorites' : 'Add to favorites'}
    >
      <Heart 
        fill={isFavorite ? 'red' : 'none'}
        stroke={isFavorite ? 'red' : 'currentColor'}
      />
    </button>
  )
}

Favorites Page

import { useFavoriteState } from '@/stores/favorite.store'
import { usePaginate } from '@/hooks/usePaginate'

function FavoritesPage() {
  const favorites = useFavoriteState()
  const { paginated, current, pages, next, prev } = usePaginate(favorites, 12)

  if (favorites.length === 0) {
    return (
      <div className="empty-state">
        <h2>No favorites yet</h2>
        <p>Start adding Pokemon to your favorites!</p>
        <Link href="/introduction">Browse Pokemon</Link>
      </div>
    )
  }

  return (
    <div>
      <h1>My Favorites ({favorites.length})</h1>
      
      <div className="grid grid-cols-3 gap-4">
        {paginated.map(pokemon => (
          <PokemonCard key={pokemon.id} pokemon={pokemon} />
        ))}
      </div>

      {pages > 1 && (
        <Pagination {...{ current, pages, next, prev }} />
      )}
    </div>
  )
}

Favorite Counter Badge

import { useFavoriteState } from '@/stores/favorite.store'
import Link from 'next/link'

function FavoritesBadge() {
  const favorites = useFavoriteState()

  return (
    <Link href="/favorites" className="relative">
      <Heart className="w-6 h-6" />
      {favorites.length > 0 && (
        <span className="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-5 h-5 flex items-center justify-center text-xs">
          {favorites.length}
        </span>
      )}
    </Link>
  )
}

Remove All Favorites

import { useFavoriteStore } from '@/stores/favorite.store'

function ClearFavoritesButton() {
  const clearAll = useFavoriteStore(state => () => {
    state.favorites.forEach(pokemon => state.toggleFavorite(pokemon))
  })

  return (
    <button onClick={clearAll} className="btn-danger">
      Clear All Favorites
    </button>
  )
}

// Or add a dedicated action to the store:
// clearAll: () => set({ favorites: [] })

Bulk Add to Favorites

import { useFavoriteStore } from '@/stores/favorite.store'

function BulkAddButton({ pokemonList }) {
  const toggleFavorite = useFavoriteStore(state => state.toggleFavorite)
  const isFavorite = useFavoriteStore(state => state.isFavorite)

  const addAll = () => {
    pokemonList.forEach(pokemon => {
      if (!isFavorite(pokemon.id)) {
        toggleFavorite(pokemon)
      }
    })
  }

  return (
    <button onClick={addAll}>
      Add All to Favorites
    </button>
  )
}

Hydration Handling

When using with Next.js, prevent hydration mismatches:
import { useHydrated } from '@/hooks/useHydrated'
import { useIsFavorite } from '@/stores/favorite.store'

function SafeFavoriteButton({ pokemon }) {
  const hydrated = useHydrated()
  const isFavorite = useIsFavorite(pokemon.id)
  const { toggleFavorite } = useFavoriteActions()

  // Show neutral state during SSR
  if (!hydrated) {
    return <button disabled>♡</button>
  }

  return (
    <button onClick={() => toggleFavorite(pokemon)}>
      {isFavorite ? '♥' : '♡'}
    </button>
  )
}

Best Practices

Prefer specialized hooks for better performance:
// Good - only re-renders when this Pokemon's status changes
const isFavorite = useIsFavorite(pokemon.id)

// Bad - re-renders on any favorite change
const favorites = useFavoriteStore(state => state.favorites)
const isFavorite = favorites.some(p => p.id === pokemon.id)
Always pass complete PokemonSummary to toggleFavorite:
// Good
toggleFavorite({ id: 25, name: 'pikachu', types: ['electric'], image: '...' })

// Bad - will cause type errors
toggleFavorite({ id: 25 })
Always check for empty favorites:
const favorites = useFavoriteState()

if (favorites.length === 0) {
  return <EmptyState />
}
Prevent SSR mismatches with useHydrated:
const hydrated = useHydrated()
const isFavorite = useIsFavorite(pokemon.id)

if (!hydrated) return <Skeleton />
return <FavoriteButton />

Build docs developers (and LLMs) love