Poke-Nex includes a favorites system that allows users to bookmark their favorite Pokemon. The system uses Zustand for state management with the persist middleware to save favorites to localStorage.
Store Implementation
The favorites store is implemented in src/stores/favorite.store.ts:
import { PokemonSummary } from '@/types'
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
type State = {
favorites : PokemonSummary []
}
type Actions = {
toggleFavorite : ( pokemon : PokemonSummary ) => void
isFavorite : ( id : PokemonSummary [ 'id' ]) => boolean
}
interface Store extends State , Actions {}
export const useFavoriteStore = create < Store >()( persist (
( set , get ) => ({
favorites: [],
isFavorite : ( id ) => get (). favorites . some (( p ) => p . id === id ),
toggleFavorite : ( pokemon ) => {
const { favorites , isFavorite } = get ()
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 )
: [ ... favorites , newPokemon ],
})
},
}),
{
name: 'POKENEX-FAVORITE-LIST' ,
}
))
Zustand Persist Middleware
The persist middleware automatically syncs the favorites list with localStorage:
Initialization
When the store is created, Zustand checks localStorage for existing data under the key POKENEX-FAVORITE-LIST.
Rehydration
If data exists, the store is rehydrated with the saved favorites. This happens before the first render.
Auto-save
Every time toggleFavorite is called, the updated state is automatically saved to localStorage.
Cross-tab Sync
Changes in one tab are reflected in other tabs through browser storage events.
The persist middleware serializes the entire favorites array to JSON and stores it in localStorage. The data persists even after closing the browser.
Hook Selectors
The store exports several custom hooks for convenient access:
useFavoriteState
Returns the entire favorites array:
export const useFavoriteState = () => {
return useFavoriteStore (( state ) => state . favorites )
}
Usage:
const favorites = useFavoriteState ()
console . log ( `You have ${ favorites . length } favorite Pokemon` )
useIsFavorite
Checks if a specific Pokemon is favorited:
export const useIsFavorite = ( id : PokemonSummary [ 'id' ]) => {
return useFavoriteStore (( state ) => state . favorites . some (( p ) => p . id === id ))
}
Usage:
const isFavorite = useIsFavorite ( pokemon . id )
This hook uses a selector to prevent unnecessary re-renders. It only updates when the favorite status of the specific Pokemon changes.
useFavoriteActions
Provides access to action methods:
export const useFavoriteActions = () => {
const toggleFavorite = useFavoriteStore (( state ) => state . toggleFavorite )
return { toggleFavorite }
}
Usage:
const { toggleFavorite } = useFavoriteActions ()
Component Integration
Here’s how the FavoriteButton component uses the favorites system:
import { useFavoriteActions , useIsFavorite } from '@/stores/favorite.store'
import { IoHeart } from 'react-icons/io5'
interface Props {
pokemon : PokemonSummary
}
export const FavoriteButton = ({ pokemon } : Props ) => {
const { toggleFavorite } = useFavoriteActions ()
const isFavorite = useIsFavorite ( pokemon . id )
return (
< button
onClick = {() => toggleFavorite ( pokemon )}
className = "p-2 rounded-full hover:bg-zinc-700/50 transition-colors"
>
< IoHeart
className = { `text-xl ${
isFavorite
? 'text-rose-500 drop-shadow-[0_0_8px_rgba(244,63,94,0.5)]'
: 'text-zinc-600 hover:text-zinc-400'
} transition-all duration-200` }
/>
</ button >
)
}
Table Row Integration
The favorites button is also integrated into the table view:
const TableRow = memo (({ pokemon } : { pokemon : PokemonSummary }) => {
const toggleFavorite = useFavoriteActions (). toggleFavorite
const isFavorite = useIsFavorite ( pokemon . id )
const handleFavoriteClick = ( e : React . MouseEvent ) => {
e . preventDefault ()
e . stopPropagation ()
toggleFavorite ( pokemon )
}
return (
< tr >
{ /* ... other cells ... */ }
< td className = "py-4 pl-4 pr-6 text-right" >
< button
onClick = { handleFavoriteClick }
title = {isFavorite ? 'Remove from favorites' : 'Add to favorites' }
>
< IoHeart
className = { `text-xl ${
isFavorite
? 'text-rose-500 drop-shadow-[0_0_8px_rgba(244,63,94,0.5)]'
: 'text-zinc-600 hover:text-zinc-400'
} ` }
/>
</ button >
</ td >
</ tr >
)
})
The click event handlers use e.stopPropagation() to prevent triggering the row’s navigation when clicking the favorite button.
Toggle Logic
The toggleFavorite function implements add/remove logic:
toggleFavorite : ( pokemon ) => {
const { favorites , isFavorite } = get ()
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
})
}
Data Structure
Favorites are stored as an array of PokemonSummary objects:
interface PokemonSummary {
id : number
name : string
types : PokeType [ 'name' ][]
image : string
}
Only essential data is stored to minimize localStorage usage.
localStorage Key
Favorites are saved under the key:
You can view the stored data in browser DevTools:
Open DevTools (F12)
Go to Application tab
Expand Local Storage
Find POKENEX-FAVORITE-LIST
Selective Re-renders Using useIsFavorite(id) with a selector ensures components only re-render when the specific Pokemon’s favorite status changes.
Minimal Data Only storing PokemonSummary instead of full PokemonDetail reduces localStorage size.
Memoization Table rows use memo() to prevent unnecessary re-renders when favorites change.
Auto-persistence Zustand persist middleware handles all serialization/deserialization automatically.
Benefits of Zustand
Simple API : No reducers, actions, or complex setup
TypeScript Support : Full type safety with minimal configuration
DevTools : Compatible with Redux DevTools for debugging
No Context Provider : Works without wrapping your app in providers
Persist Middleware : Built-in localStorage sync
Performance : Uses shallow comparison by default, preventing unnecessary renders
Cross-tab Synchronization
The persist middleware automatically handles cross-tab synchronization. When you add/remove a favorite in one tab, all other tabs update automatically through the storage event listener.
This is built into Zustand’s persist middleware and requires no additional code.