Edit: I fixed some issues, which I detail here
Project Here
More Pokemon! 100 Days of Code Days 11 - 13
I probably should have done a bit bigger project by now, but I like the idea suggested to me by my friend, that I do the pokedex of the hack(?) we're working on called Gnosis! (Is it still a hack when it's using the decomp & disassembly of emerald?) These are the native pokemon you can find in the Tenso region. There are some different forms and mega evolutions, but for the purpose of this dex we're just pulling from the vanilla entries once again from PokéAPI. While there is a lot of data that I can pull from, I decided since some will vary in a new game, I would just pull the natural qualities and typing from them. Other than that my only goal is to be able to 'search' for related entries, so let's get started!
Displaying Sprites
At first I wanted to use this resource in it's entirety, but I couldn't quite figure it out, so I just ended up grabbing the main list of named menu sprites for the pokemon to display. I'm really picky about having everything match, and since the transition to 3D models, sprite resources and split between the 3D models and the old sprites, while PokeApi links to a sprite resource that uses both. There's also fanmade resources to spriteify them all, but quality varies. That's just no good to me, so the one consistent image source for Pokemon is the menu sprites, no matter if the game is in 3D or not. Here's hoping Sword and Shield don't break the trend!
At this stage, I just have an array of the 200 pokemon in the dex. I iterate over my entry component 200 times, and store them in an array of JSX elements. This will attempt to rerender in it's lifecycle, so instead of pushing to the array, we set the specific instance based on it's ID. In the entry we have a PKMNSprite component that just grabs the related name array to it's ID, and uses that as a name for the .pngs of the sprites I saved.
for (let i = 0; i < 200; i++) {
entries[i] =
<PKDEXEntry
pkmnID={i}
key={`entry-${i + 1}`}
/>
}
...
export default function PKMNSprite(props) {
return <img
src={`${process.env.PUBLIC_URL}/regular/${GPOKEDEX[props.pkmnID].toLowerCase()}.png`}
alt={GPOKEDEX[props.pkmnID]}
/>
}
Getting from PokéAPI
While it may not be ideal, I've let the child component handle the request for information. The reason this isn't ideal as it was fine when it was just the entry getting it's own information, I have to report back to the App component about what information was received for the search criteria. More on that later.
Omitting some of the functions for controlling the parent's state, this is what our API call looks like:
axios.get(`https://pokeapi.co/api/v2/pokemon/${name.toLowerCase()}`)
.then(res => {
this.setState({ info: res.data });
return axios.get(res.data.species.url);
}).then(res => {
this.setState({ species: res.data });
this.setState({ loading: false });
}).catch(err => {
console.log(name + err);
})
}
PokéAPI stores it's general information in pages just for the forms of each pokemon, then goes into specifics about the species in another place, which is nicely linked from the information page. Note: The code will continue even if setState is still loading in. For this reason I use the response data to find the species url instead of this.state.info. The annoying part is however, the pokemon who have different forms and don't have a 'regular' state, put it in their name for the API call. So, I can't have it be as elegant as I want and have to add an exception before the call.
let name = "bulbasaur";
if (GPOKEDEX[this.props.pkmnID] === "Minior") {
name = "minior-red-meteor";
} else if (GPOKEDEX[this.props.pkmnID] === "Aegislash") {
name = "aegislash-shield";
} else {
name = GPOKEDEX[this.props.pkmnID];
}
At this point, I just had the colors alternate for entries. Later I would make them be connected to their color in the pokedex.
Searching
Just displaying the dex is easy enough, but I wanted to be able to group the pokemon by similar attributes. This is where things get a little messy. The initial entries components just contain the blank components with no data about what's actually in the entries.
The PKMNEntry component sends back the info from it's calls back to the parent App Component. We create two variables to store the info data and the species entries and pass these functions down as props to the Entry to fill out once it collects it's data. I could have also only made it send the data I need instead of two arrays for the two calls, but I didn't feel like that would have changed much in the end.
setInfoEntry = (pkmnID, data) => {
infoEntries[pkmnID] = data;
}
setSpeciesEntry = (pkmnID, data) => {
speciesEntries[pkmnID] = data;
}
A problem I was having is that if you try and use the buttons to search before the page has loaded all of the data, it errors out as some of the search terms are undefined. For this reason, the PKMNEntry components are loaded, but hidden, while the entire page is on a loading screen, then once the 200th pokemon loading entry in a loading array is set to true it then loads the entries properly. This is unfortunate as it loads twice, but I wasn't sure the best way around besides calling axios in the parents for each entry and passing along as props. It's a case of where you want your data to be. I might refactor it to do so, I'm not sure right now.
Once we have all the data in two arrays, the search functions bound to the parent are passed down as props into the PKMNStats Component and added to buttons. This specific one will take the color that is was given and input it into this function. We clear any previous filtered entries first, then iterate through our 200 entries array, looking to see if they match the path in the relevant information array. For this reason, search types are all different functions. I feel I could have made one function that accounted for them all, but it would be a mess of if statements for each condition, so I'd rather have them be separate.
showColorEntries = (color) => {
filteredEntries = [];
entries.forEach((entry, i) => {
if (speciesEntries[i].color.name === color) {
filteredEntries.push(entry);
}
})
this.setState({ showAll: false, showFiltered: true });
this.setPageFilter('Color', color);
}
I then swap if either the all entries, or the filtered entries are showing. A button at the top will take us back to all entries so we can try again. In the end the design was pretty simple, I didn't feel the need to make it so complex as we're just trying to display easy and quick data.
Conclusion
I really need to do a more medium sized project, but it's hard to find something interesting that hasn't been done. I say that, but my last to-do app failed spectacularly so I still need to redo something as supposedly simple and overdone as that. I'm happy with this though, I like thinking about the natural attributes of the pokemon instead of just the relevent stats. Finding all the squiggly pokemon and reading the hilariously terrifying dex entries is always a joy!
Top comments (2)
One big issue with your app is you are literally sending hundreds of requests to the poke api when someone lands on your page (two requests for each pokemon). That is inconceivable for a real app and even here it's likely that it's taking a toll on the poke api server(s).
You normally want a single requests for multiple pokemons (like
"https://pokeapi.co/api/v2/pokemon?offset=20&limit=20"
, theoffset
andlimit
parameters are useful for pagination).Apart from that, the code itself has room for improvement, among other things:
the variables
entries
,filteredEntries
... at the top level don't belong there. In react, if the UI depends on some data, this data should be in state. Now your app may seem to be working but you're actually lucky that the UI somehow remains in sync with the data, as only changes to state or props trigger a rerender (UI update). At the top level of a react app, you normally find static data which is not related to the UI.you should follow the principle of minimal state (reactjs.org/docs/thinking-in-react...) i.e store the minimal amount of data in state and do not store data that can be computed. For instance here you'd store the entries in state but you wouldn't store the filtered entries. Instead, store the filter itself and compute the filtered entries from the data and the filter. That way it's much easier to keep everything in sync, otherwise you might update the filter in state but forget to update the filtered data, etc.
promises in js resolves asynchronously and here there's no telling which one will resolve last (even if the request for pokemon 199 is fired last, the response might come back earlier than pokemon 1), hence the
setMainLoading
fn is flawed. Here to check that all the data has been loaded you could verify that each entry has been populated, or increase a counter when every response come so when it reaches 199 you know everything is loaded.Thank you for taking such an indepth look into my code, it's exactly the kind of feedback I need as well as providing solutions. I see now that I need to be more careful when handling api calls and include pagination, as well as be smarter when it comes to seeing if something has loaded or not. I will try to implement these changes!