Build a Real-time Flight Tracker with NextJS, TailwindCSS, and React-map-gl: A Step-by-Step Tutorial(Part 1)

Lawe Sosah
10 min readApr 11, 2023

--

Are you ready to explore the skies and track real-time flights like a pro? In this tutorial, we’ll build a flight tracker using NextJS, TailwindCSS, and react-map-gl, and learn how to work efficiently with APIs and render items on a map. So buckle up and let’s take off! If you’re like me, then you likely can’t help but look up to the sky each time an airplane is flying above. Technically, you could just use an existing app such as Flightradar24, but what where’s the fun in that?

What we are building

In this tutorial, we will use NextJS, TailwindCSS, and react-map-gl for our flight tracker, which I’ll call NovemberRomeo. We will also learn how to efficiently work with APIs, and how to render items on a map. We’ll be using the OpenSky Network API for this. We will also implement some basic animations using framer-motion in an a simple pre-loader component.

NextJS is my go to for projects such as this, because of its lightweight nature and powerful features. It is a powerful React framework that offers features such as server-side rendering and automatic code splitting out of the box. TailwindCSS is a popular CSS framework that provides a utility-first approach to styling, making it easier to create responsive designs quickly. react-map-gl is a React component for Mapbox GL JS, a powerful mapping platform that provides features such as real-time data visualization, geocoding, and routing. These are the steps to get up and running with TailwindCSS.

Step 1: Set up a NextJS project

To get started, we will create a new NextJS project. Next.js is a popular open-source React framework used for building server-side rendered (SSR) and statically generated web applications. Open up your terminal and enter the following command

npx create-next-app realtime-flight-tracker

This should now create a new NextJS project in a folder called realtime-flight-tracker. Next, navigate into the project directory by entering the following command:

cd realtime-flight-tracker

Step 2: Install TailwindCSS by running the commands below. You also need to install postcss and autoprefixer. Postcss is a required tool to process the CSS files and apply any necessary transformations, such as applying future CSS syntax or removing unused CSS rules. Autoprefixer, which is a plugin for postcss, is used to automatically add vendor prefixes to CSS properties, based on the latest data on browser support.

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

This should now create a tailwind.config.js file. Open the file and replace the content array with the following code:

 content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],

This code sets the path to all the template files in your project.

Finally, we can open up our globals.css file, in order to actually enable TailwindCSS in our project. Add the following code at the top:

@tailwind base;
@tailwind components;
@tailwind utilities;

Now run the following command to get our server spinning up:

npm run dev

Et voila! You should now have a NextJS app running. We can now start building out the project. Open the index.js file and delete all the default code apart from the main parent div.

Building the Header component

Our first component, is the Header. To start off, create a components folder at the root of the project.

Create a new folder called Header.js, and import it into your index.js file.

Before we go further, we can install react-icons, which is my preferred package of choice for icons.

npm install react-icons

We can now add the following code to our Header.js file:

This should now give us a simple Header. We will add the search functionality and other features later on.

Implementing Mapbox and displaying the map

If you’re still reading, congratulations(and thanks). We have now reached the key steps in this guide. We can start by installing Mapbox using react-map-gl. react-map-gl, is a library containing react components for Mapbox.

Before we can use react-map-gl to display a map, we need to set up a Mapbox account and obtain an access token.

  1. Go to the Mapbox website and sign up for a free account.
  2. Once you have created an account, navigate to the Access Tokens page and click on the “Create a token” button. Give your token a name and select the “Default Secret” scope.
  3. Copy your access token to the clipboard.
  4. Next, we need to install the react-map-gl package. Enter the following command in your terminal:
npm install react-map-gl mapbox-gl

5. Let’s now create a new component that renders a ReactMapGL component. Create a new file called in your components called Map.js components/Map.js Before we add anything to this file, create a .env.local file in the root of the project. This file will hold all the sensitive information such as security keys and access tokens.

Next, create a NEXT_PUBLIC_MAPBOX_TOKEN and paste in the access token from your Mapbox dashboard.

Mapbox dashboard containing the access tokens

Note that with NextJS, the token does not work unless you add NEXT_PUBLIC to it(took me a while to figure this out).

With our access tokens now in our project, add the following code to the Map.js file:

import { useState, useEffect, useContext } from "react";
import ReactMapGL, {
Marker,
Source,
Layer,
} from "react-map-gl";
const NewMap = () => {
const [viewport, setViewport] = useState({
width: "100vw",
height: "100vh",
latitude: -34.3239,
longitude: 137.7587,
zoom: 8,
});

const [flights, setFlights] = useState([]);

return(
<ReactMapGL
{...viewport}
className="w-[100%] h-[100%] z-0 absolute "
initialViewState={viewport}
onMove={(evt) => {
setViewport(evt.viewState);
}}
mapStyle={process.env.NEXT_PUBLIC_MAPBOX_STYLE}
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_TOKEN}
width="100%"
height="100%"
interactiveZoom={true} // Enables zoom with scroll
dragPan={true} // Enables panning
// The spread operator gets everything to that point
>

</ReactMapGL>
)
}

With this done, we can now create the function to actually fetch the data from OpenSky Network. The endpoint that we will be using is the GET /states/all. This endpoint returns a state array that contains most of the information that we need for this. The information that we need from here includes the callsign of the aircraft, it’s longitude and latitude, and its unique ICAO number, which we will use as an ID.

With this in mind we can now create the following two functions in our Map.js file.

We first need to set a radius for the data to be fetched based on the current viewport. This is to limit the number of API requests being made to OpenSky Network. In this way, we can stay within the limit of allowed API requests. I have set the radius to 500 nautical miles within the current viewport, but you can set it to whatever you like. We will add the airports and airport info in part 2 of this series

 // Getting flight data array

const fetchData = async () => {
const longitude = viewport?.longitude;
const latitude = viewport?.latitude;
const radius = 500; // radius in nautical miles
const url = `https://opensky-network.org/api/states/all?lamin=${
latitude - radius / 60
}&lomin=${longitude - radius / 60}&lamax=${latitude + radius / 60}&lomax=${
longitude + radius / 60
}&time=0`;

setFlightUrl(url);
try {
const response = await fetch(url);
const data = await response.json();

setFlights(data.states);
console.log(
"Fetch function ran succesfully for coords",
longitude,
latitude,
"and fetch url",
url
);
} catch (error) {
console.error("error", error.message);
}
};

The problem with this, is that an API request is made every time the viewport changes, which defeats the purpose of the radius function. To fix this, we can use a built-in Javascript method called setTimeout() to only fetch the data when the user has stopped moving for a specific period of time, 3 seconds in this case. Add the following function to your Map.js file.

const [mapMoving, setMapMoving] = useState(false);

useEffect(() => {
if (!mapMoving) {
const delayDebounceFn = setTimeout(() => {
fetchData();
fetchAirports();
}, 3000);
return () => clearTimeout(delayDebounceFn);
}
}, [mapMoving]);

We now need to actually render the different aircraft on the map. We can do this by mapping the flights array that we created earlier:

 
<ReactMapGL
{...viewport}
className="w-[100%] h-[100%] z-0 absolute "
initialViewState={viewport}
onMove={(evt) => {
setViewport(evt.viewState);
}}
mapStyle={process.env.NEXT_PUBLIC_MAPBOX_STYLE}
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_TOKEN}
width="100%"
height="100%"
interactiveZoom={true} // Enables zoom with scroll
dragPan={true} // Enables panning
// The spread operator gets everything to that point
>
{flights?.map((flight) => (
<div key={`flight-no${flight[0]}`} className="relative">
<Marker longitude={flight[5]} latitude={flight[6]}>
<IoMdAirplane
onClick={() => {
setFlightData(flight);
setSelectedFlight(flight);
setClicked(true);
}}
style={{ rotate: `${flight[10]}deg` }}
className={
selectedFlight[0] === flight[0]
? "text-red-600 text-[32px] cursor-pointer"
: "text-yellow-600 text-[32px] cursor-pointer"
}
/>
</Marker>
</div>
))}
</ReactMapGl>

The code above also changes the color of the selected aircraft based on whether it has been clicked on or not.

Adding a trajectory line and changing the viewport as the map is navigated

With this done, we now need to make a minor change in our ReactMapGl component to change the viewport and mapMoving state, as the map moves. We also need to add a line to show the exact trajectory of the aircraft that has been clicked on. The updated component should look like this:


const [selectedFlight, setSelectedFlight] = useState([]);
const [flightTrack, setFlightTrack] = useState(null)

const lineString = {
type: "LineString",
coordinates: flightTrack?.path?.map((point) => [point[2], point[1]]),
};


useEffect(() => {
if (selectedFlight) {
getFlightTrackData(selectedFlight[0]).then((data) =>
setFlightTrack(data)
);
}
}, [selectedFlight]);

//The above usEffect listens for when a flight is clicked, and fetches the trajectory
// data for the selected flight
return(
<ReactMapGL
{...viewport}
className="w-[100%] h-[100%] z-0 absolute "
initialViewState={viewport}
onMove={(evt) => {
setViewport(evt.viewState);
}}
mapStyle={process.env.NEXT_PUBLIC_MAPBOX_STYLE}
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_TOKEN}
width="100%"
height="100%"
interactiveZoom={true} // Enables zoom with scroll
dragPan={true} // Enables panning
// The spread operator gets everything to that point
>
{flights?.map((flight) => (
<div key={`flight-no${flight[0]}`} className="relative">
<Marker longitude={flight[5]} latitude={flight[6]}>
<IoMdAirplane
onClick={() => {
setFlightData(flight);
setSelectedFlight(flight);
setClicked(true);
}}
style={{ rotate: `${flight[10]}deg` }}
className={
selectedFlight[0] === flight[0]
? "text-red-600 text-[32px] cursor-pointer"
: "text-yellow-600 text-[32px] cursor-pointer"
}
/>
</Marker>

{/* New marker start */}

{selectedFlight && (
<Source
type="geojson"
data={{
type: "Feature",
geometry: {
type: "LineString",
coordinates: lineString.coordinates,
},
}}
>
<Layer
className="z-40"
id="flightTrack"
type="line"
paint={{
"line-color": "#FFFF00",
"line-width": 3,
}}
/>
</Source>
)}
</div>
))}
</ReactMapGl>
)

The useEffect that fetches the trajectory line, listens for when a flight has been selected, and sets the flightTrack to an array containing the coordinates/waypoints that are necessary to constructing the line. This should now look like the screenshot below:

Trajectory line and different styling for a selected Aircraft

Adding a Card over the map to show the data from Selected Flights.

We now need to create a basic card over the map to show the flight info of the selected flight. Create a new component called InfoCard.js in your components folder, and import it into your index.js.

Before we do this, I will now introduce you to another useful react concept, which is React context API.

React Context API is a way to share data across the component tree without having to pass props down manually at every level. It provides a way to pass data through the component tree without having to use props, which can be useful in complex applications with many layers of components.

In your components folder, create another folder and call it context. Inside that folder, create a new file called FlightContext.js. In this file, add the following:

Lastly, open up _app.txs(or app.js), and add your FlightProvider to make the context global.

This now makes the arrival airport, departure airport and selected flight states fully global. They can now be accessed from anywhere in our app. We will add more to this file later on. Before we go further, let’s update our Map.js file to use the state variables from contextAPI.

Change the selectedFlight, and clicked state variables to use useContext instead of useState like this:

  const [selectedFlight, setSelectedFlight] = useContext(SelectedContext);
const [clicked, setClicked] = useContext(ClickedContext);

In our InfoCard.js file, add the following code:

In the above code, the specific information for each flight is fetched when the airplane icon in Map.js is clicked. The data is fetched from the /flights/aircraft endpoint, which gets the specific information from the context we defined.

Your app should now look like this:

If you’re still reading this, thanks for following along in the basic setup to get the app up and running. We will build out more awesome features in part 2 which will include the following:

  1. Dynamic images for the InfoCard using the Pexels API
  2. Airport data including departures and arrivals
  3. Full search functionality

Conclusion

Creating a flight tracker web application can be a challenging but rewarding project. With the right tools and technologies, such as the ones we used, it is possible to build a user-friendly and efficient flight tracking application. The next installment in this series will include the features mentioned above in order to make a valuable tool for frequent flyers, travel enthusiasts, and aviation professionals alike.

Feel free to drop a comment if you have any questions. See you in part 2.

--

--

Lawe Sosah

Meet Lawe, a React/Next.js & TailwindCSS developer with a passion for aviation, cars and engineering. ✈️🚗 https://sosahbuilt.com