Pokédex
A catalogue of 1,025 entries that the site does not own: everything is fetched live from an outside service, then searched, filtered and sorted without the page ever stalling.
- My role
- Full rewrite — architecture, design, code
- Size
- ~1,100 lines
- Stack
- React 18, Vite, Tailwind, PokéAPI
- Status
- Live
Why this is here, when the subject is a game
The subject is beside the point. What matters is the shape: a site that holds none of its own data and has to show it anyway, from a service it does not control.
That is an ordinary commercial situation. A shop pulls prices and stock from a warehouse system. A studio pulls its calendar from a booking service. A courier pulls delivery statuses. Every one of them runs into the same four questions, and PokéAPI is simply a free way to answer them in public:
- What do you show while the data is still on its way?
- What do you show when the request fails?
- How do you avoid asking for the same thing five times over?
- How do you search and filter a thousand-plus items without a wait after every keystroke?
Fetching the index once, then never again
The catalogue asks the service for its full index — names and ids — a single time, and keeps it. Search and sorting then run over that in-memory list, so typing filters instantly instead of firing a request per character.
Detail is a different matter: stats, abilities, evolution chain and the rest are only fetched when a card is actually opened. Loading all of it up front would mean over a thousand requests for data most visitors will never look at.
Under both of those sits a small cache. It stores the promise of a request rather than its result, which quietly solves a problem that only shows up in the wild: when three components ask for the same thing at the same moment, they all join one request instead of starting three. If that request fails, its entry is dropped, so a retry actually retries rather than replaying the failure forever.
Caching the promise rather than the answer is a two-line decision that removes a whole category of duplicate traffic. On a client project this is the difference between a comfortable API bill and an uncomfortable one.
Filters that do not re-ask the server
Filtering by type is the one thing that genuinely needs an extra request, because membership of a type is not in the index. So the app fetches the list of ids for that type once, keeps it as a set, and everything after that is an intersection done locally. Switching back to a type you already visited costs nothing.
Sorting, favourites and text search never leave the browser at all. The list renders 24 cards at a time and extends as you scroll, and pagination resets whenever a filter changes — otherwise you land on page four of a list that now has two pages.
What the visitor sees while waiting
Not a spinner. Cards appear as grey skeletons in the shape of the real thing, so the layout is already in place and nothing jumps when the data lands. If the service is unreachable, the app settles into an empty state rather than a blank white page — a catalogue that cannot load its data should still look like a catalogue.
Small things around that: the detail view closes on Esc, the page behind it stops scrolling while it is open, and the evolution chain — which arrives as a nested tree — is flattened into plain stages before it ever reaches the interface.
The trade-offs I made
- Alternate forms are excluded. The service returns mega and gigantamax variants with ids above 10,000; they are filtered out, so the catalogue matches the main dex instead of quietly showing duplicates.
- Favourites live on the device. They are kept in
localStorage, which means they do not follow you to another phone. Real accounts would need a backend, and this project deliberately has none. - No tests. The API layer is separated from the interface precisely so it could be tested, but the tests are not written. I would rather say that than imply otherwise.