DEV Community

Cover image for Story of an IP Address Tracker App (a React.js App)
m-alptekin
m-alptekin

Posted on • Updated on

Story of an IP Address Tracker App (a React.js App)

Introduction and briefly on the “stories”

Hi. Welcome to my “stories” series.

In this series, I intend to write about my side projects and important lessons I learned from these or anything interesting (to me, which I assume it might be to you, too!!) throughout my journey to being a better developer.

I hope you like it and find it beneficial. Or at least a nice-read...

So, shall we begin?

Problem definition and why I did this project

This project is a challenge from www.frontendmentor.io. You can view the project definition page from here.

After doing my first starter project with HTML and CSS, I was seeking for a more challenging project where I can do some exercises on API integration and work with some external libraries. This project challenge was a good fit because:

  • It involved an API integration, therefore would be a good practice with async functions
  • It involved integration with leaflet.js (I already was looking forward doing a geolocation project)
  • It seemed to be a little bit challenging to my level at the time.

live URL: https://ip-address-tracker-ai.netlify.app/

note: if you encounter a problem fetching the IP info, please notify me in the comments section. There is a possibility that I might have run out of HTTP request number limit given in the free-tier usage of the geoipify service, which provides the IP based info.

Tech Stack I used for this project

Languages / framework or Library

  • Javascript and React.js
  • CSS (plain)

Libraries / API

  • React-leaflet (and Leaflet.js) library (link)
  • Geoipify API (link)
  • Dotenv (link)

My approach in developing the project, key takeaways, What I did good / bad

design phase and component architecture

According to me, any project / system / software product, whether simple or not, should be developed in accordance with "development process". Although this is by itself a long topic that can be discussed in pages, as a very simplified summary I can name the main phases as follows:

  • requirements / system specification phase
  • Pre or detailed design phase (including user stories, UI etc.)
  • integration / testing phase
  • deployment and maintenance phase

The basic functionality and simple requirements, which address the first two phases, were already given in the project definition, therefore after little consideration over them and making small additions in requirements, I started coding and creating react components.

But very soon after, I started having trouble with the development process. I could not remember which component was receiving info from which, etc. Of course, It can be easily derived from the code itself, but I realized that it was slowing me down and making me inefficient...

Therefore, I started creating this below diagram, an architecture-like schema, which I call it "component architecture" (is there a proper name?) and kept it updated during the development time.

component-architecture

Thanks to this diagram;

  • I could figure out the components and their relations (info flow, interfaces)
  • It certainly helped me to simplify the components structure, got rid of unnecessary ones and form a more simpler architecture
  • It helped me a lot in testing phase and in debugging as I could easily locate the possible location and reason of the error with a glimpse at the diagram

takeaway

I benefited a lot from this schema during the development and debugging phases. Therefore, for every project, regardless of its complexity, I decided to create "component architecture" and keep it updated during the development time. In my humble opinion, such diagrams could be beneficial for most people, too.

geoipify API integration

geoipify is a service which provides IP based info. Though it is a priced service, it allows free usage up to 1000 requests.

The API documentation is not hard. And, there would be no issues, unless I ran out of the free-tier limit (twice), due to wrong utilization of useEffect() hook (I will come to this later).

The API takes input of various types: email, ip address and domain names.

I made a simple filter to classify these (though there was no such requirement on the project definition).
To be frank, I did not want to spend too much effort in here, so probably, there are some areas to be improved.

My filter is simply like this:

  // classify whether the input data is email, domain name or ip Address
  // domain sample: google.com
  // ip sample: 8.8.8.8
  const findInputType = (data) => {
    if (data.includes("@")) {
      inputTypeParam = "&email=";
      return inputTypeParam;
    }
    if (data.match(/[0-9]./g) !== null) {
      inputTypeParam = "&ipAddress=";
      return inputTypeParam;
    } else {
      inputTypeParam = "&domain=";
      return inputTypeParam;
    }
  };

Enter fullscreen mode Exit fullscreen mode

The API gives the current location of the requester if no specific data is sent (i.e. no IP address, or email or domain). I made use of this in the first mounting of the map component. Details in leaflet.js integration part.

The important part of this integration is that, you have to do it with fetch() which is a promise-based method...

Did I say that I love promises and async functions.. They are important in Javascript and front-end development, but sometimes they can be little bit confusing...

With -possibly- subtle differences, you can either do the job with forming .then() chains or async/await keywords. I chose the latter one and also used try/catch to get a proper error message in case there is an issue.

Here is my code block to fetch the data.

// fetch data from the IP address API
  const getIpData = async () => {

    // call findInputType fnc if the user input is not empty string
    if (data !== "") {
      findInputType(data);
    }
    if (data === "") {
      inputTypeParam = "";
    }

    // if input is empty string (in the case of launch of the app)
    // the api returns the requesters current ip.
    let queryData = baseUrl + inputTypeParam + data;

    try {
      const res = await fetch(queryData);
      const returnData = await res.json();
      await setIpAddress(returnData.ip);
      await setCity(returnData.location.city);
      await setTimezone(returnData.location.timezone);
      await setIsp(returnData.isp);
      await setCoordValues([returnData.location.lat, returnData.location.lng]);
    } catch (error) {
      console.log(error);
    }
  };

Enter fullscreen mode Exit fullscreen mode

please check that, with async/await duo keywords, you have to put the fetching in a(n) (async) function and you have to call this function at a point.
I put the calling statement in the useEffect() hook. Therefore, either the incoming props.data or coordValues changes, it is supposed to call the getIpData(), which will trigger the fetch().
More on the useEffect() later.

takeaway

I had to chance to get deeper in async functions and try/catch usage. The important thing here is that, in order to use the fetched (resolved promise) info, you have to assign it to a "global" variable within the async function block.
Therefore, as you will see, all the resolved promises are "set" to the related "states" with useState().

(react-)leaflet.js integration

This service is awesome. I loved it. Though I need to admit that I had some problems in integrating the library.
In order to add this library, you need to integrate both react-leaflet and leaflet.js libraries. Please refer to the documentation for more.

the mounting of the app

At the first mounting of our page, since that this lib is mounted at the higher levels in the component hierarchy, the leaflet.js searches for a "place" to show and in case there is no place, it gives an error.

Therefore, I mounted the component with an "initial" location, which is the requesters location that the App gets from the geoipify service. As you will remember from the above section, the service gives the requester's location on a request (sent without any domain/email/ip-address data).

The info I received from the geoipify service is transmitted "upwards" up to the component "App", where I put the Leaflet component in.

I passed in the initial location info to the leaflet.js. And during the time needed to receive the info, I wrote a ternary statement for conditional rendering and returned "null" till there is the data to be displayed.

Again, I love ternary statements. One could say that they are perfect ways to apply conditional rendering in react.js.

return (
    <main>
      <div className="App">
        <Header mapCoord={coordHandler}></Header>
        {coord.length > 0 ? (
          <div className="leaflet-map">
            <MapContainer
              className="map-container"
              center={coord}
              zoom={13}
              scrollWheelZoom={false}
            >
              <MyComponent mapCoord={coord}></MyComponent>
              <TileLayer
                attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
                url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
              />
              <Marker position={coord} icon={myIcon}></Marker>
            </MapContainer>
          </div>
        ) : null}
      </div>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

Managing new coordinates

The interesting (which also challenged me) thing here is that, under the main tag (< MapContainer >), the passed-in coordinates are only for the initial positions.

When you need to change to a new coordinates, these would not be helpful. You need to have another component "< MyComponent >" and pass-in the new coordinates as props.
See it below:

// this component takes the new coords from App and changes the map center with a nice animation
const MyComponent = (props) => {
  const [lat, lng] = props.mapCoord;

  const map = useMap();
  useEffect(() => {
    map.flyTo([lat, lng], map.getZoom());
  }, [lat, lng]);

  return null;
};
Enter fullscreen mode Exit fullscreen mode

new marker

This post will be incomplete without this part. After achieving all the functionality, the design required to use a custom "marker" for the positioning on the leaflet map. I had to dive deep in the official documentation and beloved stackoverflow... Eventually was able to do it with the below code.

import L from "leaflet";
.....
 // to change the default icon of the leafler.js
  const myIcon = new L.Icon({
    iconUrl: iconLoc,
  });
....
<Marker position={coord} icon={myIcon}></Marker>
.....
Enter fullscreen mode Exit fullscreen mode

takeaway:

It is obvious but official docs are very important and in case you cannot find what you seek for, simply google it.. Most probably at stackoverflow or some other place you will be able to see the info that you request.

I definitely improved my doc-reading skills within this project.

useEffect()

I think, it can be written a lot on just this hook only and It would make this article much longer if I try to give all the details. Therefore, though this subject deserves a more delicate approach, I will try to cut it short.

In my first attempts to use this hook, I ran out of my free-tier limit (on the geoipify web service), twice, because I ended up with the infinite-rendering and in each render, I made a request to the API... It took few minutes -if not seconds- to make up to 1000 requests to the API...

Ok. let's go thru what I did, speaking on the code...

//header.jsx component

  // receives userInput from InputField comp and assigns it to userInput
  const onChangeHandler = (e) => {
    setUserInput(e.target.value);
  };

  // receives info from SubmitArrow comp, whether the 'submission' is done (button clicked or not). 
  // if button is clicked, send the userinput to InfoBox comp.
  // if button is not clicked, sent empty string to InfoBox comp.
  const ifClickedHandler = (isClicked) => {
//    setIfSubmitted(isClicked);
    if (isClicked === true) {
      setDataToFetch(userInput);
    }
  };

  // receives the coord values from InfoBox and sends it to App.js
  const coordValHandler = (values) => {
    props.mapCoord(values);
  }

  return (
    <div className="header-container">
      <h1 className="app-title">IP Address Tracker</h1>
      <div className="inputField-arrow-wrapper">
        <div className="inputField-wrapper">
          <InputField onChange={onChangeHandler}></InputField>
        </div>
        <div className="submit-arrow-wrapper">
          <SubmitArrow ifClicked={ifClickedHandler}></SubmitArrow>
        </div>
      </div>
      <div className="infobox-wrapper">
        <InfoBox 
          data={dataToFetch} 
          coordVal={coordValHandler}
          ></InfoBox>
      </div>
    </div>
  );

Enter fullscreen mode Exit fullscreen mode

In Header component, I take userinput data from the inputField component and if the submitArrow button is clicked, send this info as a props to the InfoBox Component.

In InfoBox component I fetch the data from the geoipify API according to this props.

And the fetching (async) function (getIpData()) is called from useEffect() hook:

 useEffect(() => {
    getIpData();
  }, [props.data]);

 useEffect(() => {
    props.coordVal(coordValues);
  }, [coordValues]);


Enter fullscreen mode Exit fullscreen mode

useEffect() is a tricky hook, such that:
UseEffect is run, if there is no dependency array '[]', on each rendering/re-rendering of the component OR
if you specify an empty dependency array '[ ]', on just the mounting of the component OR
in case there is a dependency array with some dependencies '[a,b]', whenever there is a change in the dependency /dependencies (a and b) and on initial rendering (mounting)...

Here I specified the props coming from the Header function as the dependency. Therefore, whenever the user enters a new data to the inputfield and submits the button, this data will be sent to InfoBox and if it is different than before, it will trigger the useEffect() hook, which in conclusion calls the fetching function...
Additionally, whenever the coordValues change after API call, this time the 2nd useEffect() runs and calls the linked function in the parent component (Header)

For more clear explanation check this link

takeaway

A not-so-explicit use case was (at that time, to me) was calling the async function to fetch data from the useEffect(). Also, choosing/entering the dependencies were critical because without them, it is inevitable to have an infinite-rendering problem (whenever useEffect runs in each (re-)render, it calls the API fetch function, which sets the state (and even state is the same as before), which in conclusion causes the re-rendering of the component which triggers the useEffect and.... OK, you got it I suppose...).
Lessons learned on my side...

DotEnv and publishing

As a last few words, although I used free-tier of the geoipify service, for good practice, I kept the API key as secret by using dotenv library.

const apiKey = process.env.REACT_APP_IP_API_KEY;

Enter fullscreen mode Exit fullscreen mode

dotenv lib enables to store the SECRETS in a .env file (which is not suggested strongly to push to github or any open repo) and use these variables with process.env. method.
When you are running the app from your local machine, you read it from .env file..
When deployed, the SECRETS should be kept within environmental variables section of the hosting service (which in my case is Netlify).

possible improvement areas

Possibly, there can be a more simpler architecture.
Also, I did not use any state management tool (Context, Redux etc.) here. I could make good use of it because there are few-levels of props info flow.
Is there anything critical / else to mention? Please comment.

conclusion

This is the end of the 'story' of my IP-tracker App. I really enjoyed developing this App. It not only helped me to reinforce / learn some core react.js concepts but also pushed me to dive deeper in and practice some advanced JS features, like promises / async functions, etc...
Additionally, I can say that, I improved my docs-reading skills, too :))

If you have come up to this point, thanks a lot!

In order to improve my contents and writing style, I kindly ask you to provide your comments, ideas etc:

  • How did you like it?
  • Do you have any suggestion to improve my blog-writing? What could I do to make it better?
  • Should I continue telling stories of my other projects? What else would you want to see?

Again, thanks.. Wish you all a happy new year.
alptekin

Top comments (0)