Build a Pokédex with React and PokéAPI 🔍
Tags: webdev, javascript, react, beginners
Published: 2024-07-02 07:05:43

I recently leveled up my React skills by building a Pokédex! This was such a fun project that I wanted to share the process with you all. The app allows users to search for Pokémon by name or ID, fetching detailed information from the PokéAPI, including their type, abilities, and game appearances. 

You can check out the live version of the Pokédex app [here](https://axelfrache.github.io/PokemonFinder).

![PokemonFinder main page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/azsu2txns1wx010q56ny.png)

## Prerequisites

- Basic knowledge of React and JavaScript
- Node.js and npm installed on your machine

## Project Setup and Installation

### Initialize the Project

First, create a new React application using Create React App:
```bash
npx create-react-app pokemon-finder
cd pokemon-finder
```

### Install Dependencies

Next, install the necessary libraries, including axios for making HTTP requests and @mui/material for UI components:
```bash
npm install axios @mui/material @emotion/react @emotion/styled framer-motion
```

## Adding Custom Fonts

If you want to add custom fonts to your project, you can include them in your project directory and import them into your CSS. Here are the steps to follow:

- Create a folder named fonts inside the `src/assets` directory.
- Place your font files inside the fonts folder.
- Create a CSS file named `fonts.css` inside the `src/assets` directory and import your fonts like this:
```css
@font-face {
    font-family: 'GeneralSans';
    src: url('./fonts/GeneralSans-Regular.woff2') format('woff2'),
         url('./fonts/GeneralSans-Regular.woff') format('woff');
    font-weight: normal;
    font-style: normal;
}

@font-face {
    font-family: 'PokemonPixel';
    src: url('./fonts/PokemonPixel.ttf') format('truetype');
    font-weight: normal;
    font-style: normal;
}
```

- Import the fonts.css file in your App.js file:
```
import './assets/fonts/fonts.css';
```

## Creating the Components

Before diving into the components, create a new folder named components in the src directory. We will place all our component files in this folder.

### Header

The Header component provides a simple top navigation bar for the app:

```javascript
import React, { Component } from 'react';
import AppBar from '@mui/material/AppBar';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
import Icon from '../assets/images/logo.png';
import '../assets/fonts/fonts.css';

class Header extends Component {
    render() {
        return (
            <AppBar position="static" sx={{ backgroundColor: '#ef233c' }}>
                <Toolbar>
                    <Box sx={{ display: 'flex', alignItems: 'center', flexGrow: 1 }}>
                        <img src={Icon} alt="logo" style={{ marginRight: 10, width: 40, height: 40 }} />
                        <Typography variant="h6" component="div" sx={{fontFamily: 'GeneralSans', fontSize: '1.5rem'}}>
                            POKEMON FINDER
                        </Typography>
                    </Box>
                </Toolbar>
            </AppBar>
        );
    }
}

export default Header;
```

### PokemonCard

The PokemonCard component displays detailed information about the Pokémon, including its name, type, abilities, and generation:

```javascript
import React from 'react';
import { Card, CardContent, Typography, CardMedia, Divider, Stack, CircularProgress } from '@mui/material';
import '../assets/fonts/fonts.css';
import { motion, AnimatePresence } from 'framer-motion';

const variants = {
    initial: { opacity: 0, y: 20 },
    animate: { opacity: 1, y: 0, transition: { type: 'spring', stiffness: 50, damping: 10 } },
    exit: { opacity: 0, y: -20, transition: { duration: 0.3 } },
};

const cardStyles = {
    display: 'flex',
    flexDirection: { xs: 'column', sm: 'row' },
    margin: '20px auto',
    width: '90%',
    minWidth: 300,
    maxWidth: 600,
    overflow: 'hidden',
    backgroundColor: '#f6f6f6',
    boxShadow: '0 0 10px 0 rgba(0,0,0,0.2)',
    transition: 'transform 0.3s, box-shadow 0.3s',
    '&:hover': {
        transform: 'scale(1.02)',
        boxShadow: '0 0 20px 0 rgba(0,0,0,0.3)',
    },
};

const contentStyles = {
    flex: '1',
    display: 'flex',
    flexDirection: 'column',
    justifyContent: 'center',
    alignItems: 'center',
    padding: 2,
    fontFamily: 'Arial, PokemonPixel',
};

const PokemonCard = ({ pokemon, loading, isShiny }) => {
    if (loading) {
        return (
            <AnimatePresence>
                <motion.div
                    key="loading"
                    variants={variants}
                    initial="initial"
                    animate="animate"
                    exit="exit"
                >
                    <Card sx={cardStyles}>
                        <CardContent sx={contentStyles}>
                            <CircularProgress />
                        </CardContent>
                    </Card>
                </motion.div>
            </AnimatePresence>
        );
    }

    if (!pokemon) return null;
    const types = pokemon.types.map(typeInfo => typeInfo.type.name).join(', ');
    const abilities = pokemon.abilities.map(abilityInfo => abilityInfo.ability.name).join(', ');
    const { generation, description, id } = pokemon;
    const imageUrl = isShiny ? pokemon.sprites.front_shiny : pokemon.sprites.front_default;

    return (
        <AnimatePresence>
            <motion.div
                key={id}
                variants={variants}
                initial="initial"
                animate="animate"
                exit="exit"
            >
                <Card sx={cardStyles}>
                    <CardMedia
                        component="img"
                        sx={{
                            width: { xs: '100%', sm: 170 },
                            height: { xs: 170, sm: 'auto' },
                            objectFit: 'contain',
                        }}
                        image={imageUrl}
                        alt={pokemon.name}
                    />
                    <CardContent sx={contentStyles}>
                        <Stack spacing={2} alignItems="left">
                            <Typography gutterBottom variant="h5" component="div" sx={{ fontFamily: 'PokemonPixel', textAlign: 'left', fontSize: '2rem' }}>
                                {pokemon.name} (#{id})
                            </Typography>
                            <Divider variant="middle" sx={{ bgcolor: '#ef233c', width: '100%' }} />
                            <Typography variant="subtitle1" component="p" sx={{ fontFamily: 'Roboto', fontSize: '1rem' }}>
                                <b>Description:</b> {description}
                            </Typography>
                            <Typography variant="subtitle1" component="p" sx={{ fontFamily: 'Roboto', fontSize: '1rem' }}>
                                <b>Type:</b> <span style={{ color: '#4A90E2' }}>{types}</span>
                            </Typography>
                            <Typography variant="subtitle1" component="p" sx={{ fontFamily: 'Roboto', fontSize: '1rem' }}>
                                <b>Generation:</b> {generation.replace('generation-', '').toUpperCase()}
                            </Typography>
                            <Typography variant="subtitle1" component="p" sx={{ fontFamily: 'Roboto', fontSize: '1rem' }}>
                                <b>Abilities:</b> {abilities}
                            </Typography>
                        </Stack>
                    </CardContent>
                </Card>
            </motion.div>
        </AnimatePresence>
    );
};

export default PokemonCard;
```

### SearchBar

The SearchBar component provides the input field and search button for the user to enter the Pokémon name or ID:

```javascript
import React from 'react';
import TextField from '@mui/material/TextField';
import Button from '@mui/material/Button';

const SearchBar = ({ onSearch }) => {
    return (
        <form onSubmit={onSearch} style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '20px', margin: '20px' }}>
            <TextField
                name="pokemonName"
                label="Enter a Pokémon name or ID"
                variant="outlined"
                fullWidth
                style={{ maxWidth: '500px' }}
            />
            <Button type="submit" variant="contained" color="primary">
                Search
            </Button>
        </form>
    );
};

export default SearchBar;
```

## Main application file

The App component is the heart of the application, managing the state and handling API requests. Here’s the code for the App component:

```javascript
import React, { useState } from 'react';
import axios from 'axios';
import { CircularProgress, FormControlLabel, Switch, Box } from '@mui/material';
import SearchBar from './components/SearchBar';
import PokemonCard from './components/PokemonCard';
import Header from './components/Header';
import './assets/fonts/fonts.css';
import './App.css';

function App() {
    const [pokemonData, setPokemonData] = useState(null);
    const [loading, setLoading] = useState(false);
    const [isShiny, setIsShiny] = useState(false);

    const cleanDescription = (description) => description.replace(/\f/g, ' ');

    const fetchPokemonData = async (pokemonNameOrId) => {
        setPokemonData(null);
        setLoading(true);

        try {
            const sanitizedInput = pokemonNameOrId.toLowerCase().replace(/^0+/, '');
            const baseResponse = await axios.get(`https://pokeapi.co/api/v2/pokemon/${sanitizedInput}`);
            const speciesResponse = await axios.get(baseResponse.data.species.url);

            const generation = speciesResponse.data.generation.name;
            const flavorTextEntries = speciesResponse.data.flavor_text_entries.filter(entry => entry.language.name === 'en');
            let description = flavorTextEntries.length > 0 ? flavorTextEntries[0].flavor_text : 'No description available.';
            description = cleanDescription(description);

            setPokemonData({
                ...baseResponse.data,
                generation,
                description
            });
        } catch (error) {
            window.alert('Pokémon not found. Please try a different name or ID.');
        } finally {
            setLoading(false);
        }
    };

    return (
        <div className="App">
            <Header />
            <SearchBar onSearch={(e) => {
                e.preventDefault();
                const pokemonName = e.target.elements.pokemonName.value.trim();
                if (pokemonName) fetchPokemonData(pokemonName);
            }} />
            <Box sx={{ display: 'flex', justifyContent: 'center', mt: 1 }}>
                <FormControlLabel
                    control={
                        <Switch checked={isShiny} onChange={(e) => setIsShiny(e.target.checked)} color="primary" />
                    }
                    label="Show Shiny"
                />
            </Box>
            {loading && (
                <div style={{ display: 'flex', justifyContent: 'center', marginTop: '20%' }}>
                    <CircularProgress />
                </div>
            )}
            {pokemonData && <PokemonCard pokemon={pokemonData} isShiny={isShiny} />}
        </div>
    );
}

export default App;
```

## Conclusion

Congratulations, you have successfully built a Pokédex using React and the PokéAPI! You can now search for any Pokémon by name or ID and view detailed information about them.

Feel free to explore and add more features to your app, such as displaying Pokémon stats or comparing multiple Pokémon. For more details and to contribute to this project, check out the [Pokemon Finder repository on GitHub](https://github.com/axelfrache/PokemonFinder).