Natural Pokedex Refactor

misnina

Nina

Posted on April 1, 2019

Natural Pokedex Refactor

Project I'm Talking about: Here
Original Article:

What went wrong?

I feel a little embarrassed! It seems I was very much mismanaging and over pulling from the API in my last build. I was given a very informative comment on all the issues and I set out to attempt to address all of them.

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", the offset and limit 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.

Starting from (almost) scratch

I realized I need to do all my information handling from the top most parent and pass down the attributes instead of letting the entry itself handle the data calling. The embarrassing part is that the comment doesn't reach the fact that the data call was refetching everytime it reloaded the components, which was every time you did a search. I don't know how I missed this really, I should have realized that having each component rerender on search was stupid, and honestly just wastes a lot of time.

So let's start using states, and only states! I can store all the information I gather from the top level, and only call the data to store once. The offset and limit were a good tip for other apis in the future, but unfortunately this pokedex isn't in a sorted listed in the api, and to sort by type I need to match against the data I've collected, not just which ever one is displaying at the time. Anyways, I decided to cut out the types, as it was the only attribute I was getting from the original dex entry, and not the species dex, so I also got to cut out that gross filter for my initial calls.

fetchPokemon = (name, id) => {
    axios.get(`https://pokeapi.co/api/v2/pokemon-species/${name.toLowerCase()}/`)
      .then( res => {
        const info = res.data;
        let eggSecond;
        info.egg_groups[1] ?
          eggSecond = info.egg_groups[1].name : eggSecond = null;

        let enEntries = info.flavor_text_entries.filter(entry => {
          return entry.language.name === 'en'
        });
        const data = {
          name: name,
          id: id,
          color: info.color.name,
          eggGroups: [
            info.egg_groups[0].name,
            eggSecond,
          ],
          genus: info.genera[2].genus,
          shape: info.shape.name,
          flavorText: enEntries[0].flavor_text,
        }

        this.setState(prevState => {
          const allEntryInfo = [...prevState.allEntryInfo, data];
          return {allEntryInfo};
        });

        this.setState({ counter : this.state.counter + 1 })
      }).catch (err => console.log);
  }

}
Enter fullscreen mode Exit fullscreen mode

Fetch less

Since we'll be storing the data ourselves and calling upon it, might as well format it before we put it in. One thing about states is that they should be immutable, so I had some figuring out to do to do that, and I flailed around a bit to do it, but I think I get it now.

When you setState, you can instead pass a function with the current state's snapshot in time as a variable. So you set the new state to be an array of all previous entries, plus a new one with the data we've collected, then return that new setup for the setState to take in. The spread (...) was a tricky one when I first saw it, but it basically means that it can be zero or any number of items, which is handy! In addition to this, we have another state variable that counts up each time an entry is gone through.

Once we have our data, we can move on to actually rendering, and figuring out when to render. Because setState rerenders each time it's called, we're actually rerendering a blank page 200 times, which I don't know if it has actual performance issues, but it's somewhat annoying feeling. For this reason, we can edit our shouldComponentUpdate feature.

  shouldComponentUpdate() {
    if (this.state.counter >= 199) {
      return true;
    } else {
      return false;
    }

Enter fullscreen mode Exit fullscreen mode

If the counter hits 199, then the component should update when it receives new props or states. Easy enough, now let's add on to our fetchPokemon call to make an object containing the JSX elements we need to render.

const entry = <Entry
          id={id}
          name={name}
          key={name}
          color={data.color}
          egg1={data.eggGroups[0]}
          egg2={data.eggGroups[1]}
          genus={data.genus}
          shape={data.shape}
          flavorText={data.flavorText}
          setSorting={this.setSorting}
        />;
      this.setState(prevState => ({
        allEntryComponents: {
          ...prevState.allEntryComponents,
          [name] : entry
        }
      }))
Enter fullscreen mode Exit fullscreen mode

Out of Order

I make this an object with the name as a key for a reason. The reason is this.

It really is true, the calls do get out of order and return at different times. This is a mess, but I don't know of a way to force them to go in order, and is it worth the wait? We can just sort them after, but for the components we need them to be named to know which pokemon is stored where as we can't access their id number once they're sorted as an element.

  sortBy(type, value) {
    const { allEntryInfo, allEntryComponents } = this.state;
    let sorted = [];
    if (type === 'all') {
      GPOKEDEX.forEach(pokemon => {
        sorted.push(allEntryComponents[pokemon]);
      });
    } else if (type === "eggGroups") {
      let typeSorted = allEntryInfo.filter(pokemon => {
        return pokemon[type][0] === value || pokemon[type][1] === value ;
       });
       typeSorted
         .sort((a, b) => a.id - b.id)
         .forEach(pokemon => {
           sorted.push(allEntryComponents[pokemon.name])
         });
    } else {
      let typeSorted = allEntryInfo.filter(pokemon => {
       return pokemon[type] === value;
      });
      typeSorted
        .sort((a, b) => a.id - b.id)
        .forEach(pokemon => {
          sorted.push(allEntryComponents[pokemon.name])
        });
    }
    return sorted;
  }
Enter fullscreen mode Exit fullscreen mode

While we're sorting by number, we can be smarter about how we do our searches for different types. First, for all, we just take the order from the GPOKEDEX listing, and push to the corresponding name in allEntryComponents. For the other ones, when setting the sortBy, we take arguments 'sortType and sortValue', meaning we want to do sortBy('color', 'green') for example. These are also stored in the top-level state, so the component rerenders when called, and also displays what you're looking at in the header. For egg groups, the only difference is that we OR compare two of the values, as they can match the first or second place.

While we are trying to sort the components, we use the info placement first so we can get at the inner data. We filter to only get the ones where the type matches the value set. One we have that, we call two things, sort and a forEach. Sort compares each entry with the other, and sees if the the id is higher or lower then each of them, and then returns them in proper order. So now we have our data, filtered by the type, and sorted by ID, but now we need the related components! For each element in the array, we push to the sorted array the corresponding entry component with the pokemon's name again.

Now we're getting somewhere!

Etc.

I had some troubles with a missing value, or rather a missing level in an object, so it was returning an object and React really doesn't like that, but can't pinpoint what line it's on so I had some time figuring it out. Since I had all the styling done, that's about it! Just had to reset a few things, and because react is react, I could just reuse a lot of components.

So now instead of an embarrassing amount of api calls (400 x whenever it reloaded), we call 200 for the entire limetime you are on the page. Also the searches are very fast because they're already loaded!

What's next?

I still haven't been able to wrap my head around Redux, but I was seeing some stuff about sorting the information, possibly in local storage, so it wouldn't even need to recall that 200 until you cleared your data. I need to research a bit more about it.

💖 💪 🙅 🚩
misnina
Nina

Posted on April 1, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related