Add interactivity to your web apps with React

Author:Murphy  |  View: 25752  |  Time: 2025-03-23 19:46:00

Maps are a powerful tool for visualizing and understanding geographic data but they need specific skills to be designed efficiently.

In this step-by-step guide, we are going to take a deep dive into building a map-based application to show the customers' prices of gas stations around them. We will cover the different key steps of a product, from original proof of concept (POC) to the minimum viable product (MVP)

Articles in the series:

Part I: The proof-of-concept – Build a minimalist demo

Part II: How to use React to build web apps (Static Layout)

Part III: Add interactivity to your web apps with React

Part IV: Build a back-end with PostgreSQL, FastAPI, and Docker

A bit of context around this article

This article is the direct continuation of Part II in which we started to build the UI of the web app using React.

In the previous article, we started to explore the potential of React to build web applications and in particular for our Gas Station Finder. We ended up with a nice application layout that can be run locally on a test-server locally, but nothing yet interactive to really navigate through our data. I will not go over what we covered in part II and I strongly recommend going through it first as they are part of a whole.

In this post, we are going to finalize the React component and conclude on the client-side part of our product. In particular, we are going to cover the following topics:

  • Managing and passing states from component to component
  • Import and consume data from an API endpoint
  • Update dynamically a page with new data
  • Build a few interactive components (slider, special text box…)
  • Handle API errors

By the end of the article, our application will be fully reactive, and the next step will be to look at the server side of the application.

As usual, you can find the full code covered in this article on the related GitHub page.

Side note: Things are changing fast. I wrote this article in March 2023 and I am using React 18.2.0. If you read this article years after its publication, it might be a bit outdated. So be careful with what you will read below.


A bit of theory behind React state management

In modern React, we define components just as "fancy" asynchronous functions. Those run perpetually and "listen" to some events (for example, a user clicking on it, the mousse passing on it, etc…) to execute stuff when an event happens (like modifying a variable that will update what is displayed in the screen – we call those callback functions).

To make the magic happens, the variables that are meant to be modified by callbacks to change the rendering of the component have to be defined in a special way, and we call the "state" variables (they manage the state of the application). When those variables are modified, all the code inside the component is rerun, which can result in changes in the user screen.

Define "state" variables

Let's have a look at how to define those state variables.

We start with a very simple example which creates a button that changes color when a user clicks on it:

import { useState } from "react";

export default function App() {

  const colors = ["green", "red"];
  const [colorIdx, setColorIdx] = useState(0);

  const onClickCallBack = () => setColorIdx(1 - colorIdx);

  return (
    
); }

Let's check the code line by line.

First, we import the "useState" hook, which will be used to create the state variable that can be modified by callbacks. I will not go into the details, but a "hook" is simply a special feature that helps build React interactive components out of functions. We will cover two "hooks" in this article, but many more exists for a lot of different use cases…

import { useState } from "react";

Then, we create our state variable using the useState hook. When you create a variable this way you are actually creating two things:

  • the variable itself (in our case: colorIdx)
  • A "setter": a function that will allow you to modify properly your variable (setColorIdx)

useState(0) means that we initiate our variable colorIdx to 0.

const [colorIdx, setColorIdx] = useState(0);

We can then create what we call in React an "arrow function" to define the callback that will modify the variable colorIdx. An arrow function is nothing more than a function with a simpler syntax, which is particularly convenient for defining callbacks. The equivalent in python would be the lambda functions.

const onClickCallBack = () => setColorIdx(1 - colorIdx);

The value put inside setColorIdx will become the new value of colorIdx. In the case above, every time we call onClickCallBack, we change the value of colorIdx from 1 to 0 or from 0 to 1.

Then, we bind our callback to the "onClick" event of the button, so that every time the user clicks on it, it will result in changing the index colorIdx.

This colorIdx is then used to modify directly the color style of the button, in such a way that it changes from "red" to "green" with every click.

The useEffect hook

The logic above will work well most of the time. The state of a variable is modified after a particular event, it re-triggers all the code inside the component, and the display on the screen is refreshed.

Nevertheless, in some cases, you don't want all your code to be re-run when a state variable has been modified. This is the case for example when you make an API call and download data at the first render of your widget. This data might be useful to be downloaded only one time, but once the widget is rendered the first time, you don't want the call to be made multiple times every time a state variable is modified.

Let's take again the example of our button, I simply add a log to illustrate the code being rerun at every click:

export default function App() {

  ... 

  console.log("click");
  console.log(colors[colorIdx]);

  return (...)

The avoid this issue, React proposes the useEffect hook. The idea of that hook is that every line of code defined inside will be executed only at the initialization of the component or when the state variables from a watch list are updated.

useEffect takes two parameters:

  • one function: The code that will be executed during initialization and when certain state variables are modified
  • one array (the "watch list") is used to control the re-run of the code inside the function.

In the example below, the console.log("click") will be executed only at the initialization of the widget as the watch list is empty.

import { useState, useEffect } from "react";

export default function App() {

  ...

  useEffect(() => {console.log("click")}, [])

  console.log(colors[colorIdx]);
  return (...)


Retrieve data for our Gas Station Finder app

We have now all the theoretical material to add reactivity to our components.

The first thing we want to do is to retrieve the filtered data around a given postal code and for a particular gas type. To do this we will need several things:

  • Create a state variable that will contain all the information of the stations, that can be used by the table and the graph
  • Make a callback that will trigger an API call and update the relevant state variable

Dropdown

Let's start with the Dropdown. In the previous article, the component used to be static and nothing would happen if a user was selecting another gas type.

As of now, the value in the tag is hard-coded to "SP95". We are going to replace it with a state variable using useState:

const [gasType, setGasType] = useState("SP95");

We now need to modify the value when the "onChange" event is fired in the dropdown:

const handleChangeDropdown = (event) => {
  setGasType(event.target.value);
};

The button "Find Stations"

When the button is hit, we want to pass the information from the Dropdown and the TextArea to an API that will send back stations, prices, and metadata in a 30km radius around the city of interest.

Clicking on a button can be accessed via another callback named onClick, which is fired when a left click happens on it.

Let's prepare our function for now and simply log postalCode and gasType.

const handleButtonClick = () => {
  console.log(`Postal Code: ${postalCode}, Gas Type: ${gasType}`)
}

A word about the API

In the following article, we will discuss more in detail how to set up the server-side API using Python + FastAPI. I assume for now that the service is already available and that it provides a GET endpoint that provides all the data needed from a postal code and a gas type.

For example, use the below URL:

http://API_URL/stations?oil_type=SP95&postal_code=60560

will result in sending the metadata of all stations in a 30km radius as well as lat/lon from the center of research and the circle coordinates that will be consumed to generate the graph and the table.

{
  "lat": 49.1414,
  "lon": 2.5087,
  "city": "Orry-la-Ville",
  "circle_lat": [49.411,49.410,...],
  "circle_lon": [2.5087,2.5347,...],
  "station_infos": [
    {
      "address": "Centre Commercial Villevert 60300 Senlis",
      "price_per_L": 1.88,
      "price_tank": 113,
      "delta_average": 7,
      "better_average": 1,
      "google_map_link": "https://www.google.com/maps/search/?api=1&query=Centre+Commercial+Villevert+60300+Senlis",
      "distance": 10,
      "latitude": 49.221,
      "longitude": 2.591
    },
    {
      "address": "Rue de Pontoise 95540 Méry-Sur-Oise",
      "price_per_L": 1.9,
      "price_tank": 114,
      "delta_average": 6,
      "better_average": 1,
      "google_map_link": "https://www.google.com/maps/search/?api=1&query=Rue+de+Pontoise+95540+Méry-Sur-Oise",
      "distance": 26,
      "latitude": 49.061,
      "longitude": 2.17
    },
    ...
  ]
}

Passing information from one component to another

Another thing to discuss now before going further is the way to pass the information from our StationsFilter component to our App component.

In React, information can be propagated from parent to children using "props". This "props" is an object (key/values) that will contain the different elements passed as attributes in the tag.

In the example below we access a variable in the Child component via props.childVariable.

// A parent component, made of a state variable and a Child component
export default function Parent() {

  [variable, setVariable] = useState(0)
  return ( 
     
  )
}

// We pass "props" as parameter of our children component
// This props is an Object (key/value) which contains all the attributes passed
// in the parent component
export default function Child(props) {
  //We access the variable via props.childVariable
  //It will return the current value stored in childVariable, which is in this case 0
  console.log(props.childVariable)
}

This way, we can propagate values from Parent to Child. But what about the other way around? If you modify a value in Child that you would like to pass to Parent, you can do exactly as above, but instead of passing the variable, you can pass the setter. And thus, when a callback is fired in Child, it can fire the setter from Parent, which will modify the variable define in Parent.

export default function Parent() {

  [variable, setVariable] = useState(0)

  return ( 
     setVariable(newVal)}/> 
  )
}

export default function Child(props) {
  

In the snippet above, every time a click is done, we fire a function that takes the current variable (in props.childVariable), adds +1, and fires the setter that will set "variable" in Parent to variable+1.

Finalizing our API call

With this bit of theory in mind, let's finalize our component. We start by creating multiple state variables in App.js that will be used to store the information from the API.

const [stationsData, setStationsData] = useState([]);
const [latCity, setLatCity] = useState(0);
const [lonCity, setLonCity] = useState(0);
const [latCircle, setLatCircle] = useState([]);
const [lonCircle, setLonCircle] = useState([]);

We then pass the setter to our StationsFilter component, where we will load the data from the API.



//And don't forget to modify the definition of StationsFilter in StationsFilter.js 
//to include the props object !

//In StationsFilter.js:
export default function StationsFilter(props) {...}

We need now to prepare the API call. In React, this is done using the "fetch" method, which takes an URL (for the requests) as well as extra parameters such as headers or extra request parameters. Let's implement it directly in our "HandleClick" function used earlier.

const handleButtonClick = () => {
  fetch(`${APIURL}/stations?oil_type=${gasType}&postal_code=${postalCode}`)
    .then((res) => {
      return res.json();
    })
    .then((data) => {
      props.setStationsData([...data["station_infos"]]);
      props.setLonCity(data["lon"]);
      props.setLatCity(data["lat"]);
      props.setLonCircle([...data["circle_lon"]]);
      props.setLatCircle([...data["circle_lat"]]);
    });
};

Going line by line:

  • We first use "fetch" which, by default, makes a GET call to the URL passed in the parameter. In our case, we form the URL to include the state variable gasType and postalCode
  • fetch is an asynchronous function, we need to wait for it to complete before processing the data sent by the API. To do so, we use the callback .then() that we chain to our API call. When the fetch call is completed, it will automatically run the code in .then(). In that case, we receive a Response (from the API) that we convert into JSON using res.json(). This is also an asynchronous function, so processing its results has to be done by chaining another .then() callback
  • In the last .then(), we retrieve the data parsed as JSON and we simply unpack it to lat, lon, and stations_infos. We then fire the different setter defined in App.js to update the different state variables. Note that in the case of stationsData, we use the spread operation [someArray] which will create a copy of someArray. This is to make sure we create a new array to pass to stationsData and not just a reference to someArray which could cause some unexpecting behaviors.

Clicking on the button will now fire the API call that will ultimately update the state variables in App.js.

Connect the StationsTable component

If your recall from previous the article, we built the preview of StationsTable by using a pre-loaded JSON, which is following exactly the same format as what is stored now in the stationsData variable.

Downloading the data from the API and storing them in stationsData was the difficult part. Now we just need to modify slightly our StationsTable component to use stationsData instead of the pre-loaded JSON.

First, we add an attribute to StationsTable the state variable stationsData

We then add props to the declaration of the function in StationsTable.js

export default function StationsTable(props) {...}

Finally, we replace the pre-loaded JSON with the stationsData contained in the object props:

 
     {props.stationsData.map((row) => {...})}
 

And that's it, the data in the table will be replaced by the prices of stations around the city of interest every time we click on the button.

Generate the Map with the state variables

You should start to be used to it by now, we are going to proceed as before: pass the data from App to StationsMap via the attributes of the tag, add props to StationsMap, and modify the relevant part of each of the trace we have pre-generated in the last article.

Now in StationsMap.js, we will create the different traces one by one, following the template provided by the python figure (from Part I of this series), and replace the relevant data when needed.

I show you here two examples below, then it becomes a bit redundant.

The trace for the circle around the perimeter of the research:

const circleTrace = {
  fill:"toself",
  fillcolor:"rgba(1,1,1,0.2)",
  lat:props.latCircle,
  lon:props.lonCircle,
  marker:{"color":"black","size":45},
  mode:"lines",
  opacity:0.8,
  showlegend:false,
  type:"scattermapbox",
  uid: uuidv4()
}

The black border of the oil stations:

const stationsBorder = {
  lat: props.stationsData.map((e) => e["latitude"]),
  lon: props.stationsData.map((e) => e["longitude"]),
  marker: { color: "black", size: 45 },
  mode: "markers",
  opacity: 0.8,
  showlegend: false,
  text: props.stationsData.map((e) => e["price_per_L"].toString() + "€/L"),
  type: "scattermapbox",
  uid: uuidv4()
};

Note: Mapbox traces require unique identifiers for each trace. To do so, we can use a very convenient toolbox called uuid which can generate random ids for us.

npm install uuid
import { v4 as uuidv4 } from "uuid";

When all our traces are defined, we can put them in a list that will be used as an attribute for our plotly component:

const data = [circleTrace,stationsBorder,stationsPriceColor,
              stationIconsTrace,pointLocation];

Now we can take care of the chart layout, using the same method: we take the dictionary exposed by the python figure and replace the "hardcoded" attributes with our state variables to make the layout interactive. In particular, we need to modify the "lat" and "lon" of the center of the layout to make sure that each time a user makes a request, the map will automatically recenter on the city of that request.

const { REACT_APP_API_KEY } = process.env;

const layout = {
  mapbox: {
    accesstoken: REACT_APP_API_KEY,
    center: { lat: props.latCity, lon: props.lonCity },
    style: "streets",
    zoom: 11,
  },
  margin: { b: 0, l: 0, r: 0, t: 0 },
  autosize: true,
};

Also, note something very important here: we are passing our Mapbox access token in the layout. This is something you don't want to expose publicly, so you should NEVER hard-code it somewhere, or it could be usable by other people for other purposes.

To add any secret variable in your project, you should add them within a .env file in the root of your project:

fuel-station-front/
  |-- node_modules/
  |-- public/
  |-- src/
  |-- package.json
  |-- package-lock.json
  |-- README.md
  |-- .env

The .env should be added to your .gitignore in such a way that you cannot push it accidentally to a public place.

In React, you need to respect the prefix REACTAPP which will be the only variables read by the framework.

In our case, our .env looks simple like this:

REACT_APP_API_KEY=pk.auek...

With our "data" and "layout" directly plugged into our states variable, we can simply generate our plotly graph the same way as we did in the previous article.

  return (
    
);

Let's have a look at the web app in action!


Improving the details

Our app answers now the main "user story": from a type of oil and a postal code, we are able to retrieve and display information about the prices from the surrounding stations. And it is already a great step achieved. We would like now to improve a bit the user experience with a few improvements that will be detailed in this section.

Initiate the Application with an API call

Our current version of the app initiate in the middle of nowhere, in the middle of the sea. This can be a bit disturbing for a user that would arrive there for the first time. In order to remove this, we are going to initiate the application with an API call around Paris for SP98.

To do so, we are going to use the useEffect "hook" that we described in the first part of this article.

To do so, we start by passing the API call logic in App.js:

const ApiCallAndUpdateState = (gasType, postalCode) => {
  fetch(`${APIURL}/stations?oil_type=${gasType}&postal_code=${postalCode}`)
  .then((res) => {
    return res.json();
  })
  .then((data) => {
    setStationsData([...data["station_infos"]]);
    setLonCity(data["lon"]);
    setLatCity(data["lat"]);
    setLonCircle([...data["circle_lon"]]);
    setLatCircle([...data["circle_lat"]]);
  });
}

We then wrap this function in a useEffect hook with gasType="SP98" and postalCode = "75001" which corresponds to the city center of Paris.

useEffect(() => {
  ApiCallAndUpdateState("SP98","75001")
}, [])

As I pass an empty list in the second parameter of the useEffect, this bit of code will only be executed at the component initialization, which is exactly what we want.

Finally, now that we embedded all our update call logic in a function, we can do a bit of refactoring to our StationsFilter component by passing this function instead of all the setters for our state variables.

And in StationsFilter.js, we just trigger this new function in the "HandleButtonClick" callback:

const handleButtonClick = () => {
  props.ApiCallAndUpdateState(gasType, postalCode)
};

Adding a distance slider

In the current version of the app, we are displaying on the screen all the stations in a 30km perimeter, which can make sense in low-density areas but that can also be really heavy for high-density areas.

To solve this issue, we are going to use a slider to control the perimeter of the research, from 1 km to 30 km. With React we have access to a large ecosystem of libraries and tools, and after some research, I found a slider component that would fit well in the Gas Finder App here. I'll not detail the styling here, only the interactions.

Let's start by installing the react library:

npm i react-slider

We can import the component simply in our project:

import ReactSlider from "react-slider";

The slider will control another state variable (the filtering distance). Let's add it to our App.py. I initiate it to 5km as a starting point.

const [distanceFilter, setDistanceFilter] = useState(5);

We will integrate the slider between the StationsFilter and the StationsTable.


 (
    
{state.valueNow}
)} onAfterChange={(e) => setDistanceFilter(e)} />

I will not go too much into the details here and pass on the .css, the component takes many parameters such as classes with the thumb and track, and minimum and maximum values. I modified the values based on the examples from the documentation.

Two interesting things to note:

  • As for our Dropdown, we can control the value of the slider via our state variable.
  • We will modify distanceFilter by using another callback: onAfterChange which will fire only after the user stops moving the thumb.

At that point, our slider control distanceFilter, and we now need to use it to filter the data points. To achieve this, we need to create another array, which will be a filtered version of stationsData (the data taken from the API).

const filteredData = stationsData.filter(stationPrice => stationPrice.distance <= distanceFilter);

We can now simply pass this filteredData instead of stationsData to our components and it will automatically update the stations visible based on the distance.

There is one last thing to do: control the trace that shows the circle showing the perimeter of the research. Currently, the circle is based on the output sent by the API based on a 30km distance.

There are two ways to make the modification: we could create a new API endpoint that would send us the array of points based on a lat/lon/radius and keep the logic of creating the circle in the backend, especially if you are more comfortable with python. This solution has the massive disadvantage that it will multiply the number of API calls every time a user plays a bit with the slider, which is something we would like to avoid for performance purposes.

The other solution is to transpose our python function to calculate the circle in Javascript, which will be calculated directly on the client side of the application. We will go with that second option for performance reasons.

Let's start by creating a new utils folder located in StationsMap and containing a file drawCircle.js:

    |-- StationsMap/
      |-- StationsMap.js
      |-- StationsMap.css
      |-- utils/
        |-- drawCircle.js

I will transpose exactly the function that we used in python in Part I, but this time, in Javascript:

function calcPointsOnCircle(lat, lon, radius, numPoints) {
  const points = [];
  const R = 6371;

  for (let i = 0; i < numPoints; i++) {
    const bearing = (360 / numPoints) * i;
    const lat2 = Math.asin(
      Math.sin(toRadians(lat)) * Math.cos(radius / R) +
        Math.cos(toRadians(lat)) *
          Math.sin(radius / R) *
          Math.cos(toRadians(bearing))
    );
    const lon2 =
      toRadians(lon) +
      Math.atan2(
        Math.sin(toRadians(bearing)) *
          Math.sin(radius / R) *
          Math.cos(toRadians(lat)),
        Math.cos(radius / R) - Math.sin(toRadians(lat)) * Math.sin(lat2)
      );
    points.push([toDegrees(lat2), toDegrees(lon2)]);
  }

  points.push(points[0]);

  return points;
}

function toRadians(degrees) {
  return (degrees * Math.PI) / 180;
}

function toDegrees(radians) {
  return (radians * 180) / Math.PI;
}

I will not go into the detail of that formula, it is a basic mathematical operation and is not relevant to this article. The main function, calcPointsOnCircle, is taking a lat/lon representing the center of the circle, a radius, and the number of points we desire. It returns an array made of tuples of (lat, lon).

An important point nevertheless if you are new to Javascript is that to make your function usable by other files, you need to export it at the end of the file.

export { calcPointsOnCircle };

The function can be now used directly in StationsMap to calculate interactively the circle based on the current state variables (lat/lon and selected distance). Let's have a look at the code modifications.

import { calcPointsOnCircle } from "./utils/drawCircle";
...

export default function StationsMap(props) {

  // We use our function to create an array of points based on the state variables
  const pointsOnCircle = calcPointsOnCircle(
    props.latCity,
    props.lonCity,
    props.distanceFilter,
    100
  );

  // We use map() to extract all the lat and all the lon and pass them to the 
  // circleTrace
  const circleTrace = {
    fill: "toself",
    fillcolor: "rgba(1,1,1,0.2)",
    lat: pointsOnCircle.map((e) => e[0]),
    lon: pointsOnCircle.map((e) => e[1]),
    marker: { color: "black", size: 45 },
    mode: "lines",
    opacity: 0.8,
    showlegend: false,
    type: "scattermapbox",
    uid: uuidv4(),
  };

  ...

This concludes this section. By now, we have an interactive slider that controls the data displayed on the screen based on a filter on the distance.

Keep current zoom level

At this point, every time a user play with the slider, it will reset the plotly view and regenerate the layout. The problem with this is that it also reset the zoom, which is a behavior we want to avoid, see the illustration below.

One way to tackle the problem is to control the zoom with the slider value so that it automatically adapts to the circle size. In our case, this is a very easy modification to perform as we are incrementing distanceFilter 1 by 1. Thus, the simplest way is to create a mapping {current distance value -> zoom wanted} and use it to automatically control the zoom level. In StationsMap:

const mapDistZoom = {
  1: 14.0,
  2: 13.45,
  3: 12.9,
  ...
};

...
const layout = {
  mapbox: {
    accesstoken: REACT_APP_API_KEY,
    center: { lat: props.latCity, lon: props.lonCity },
    style: "streets",
    zoom: mapDistZoom[props.distanceFilter],
  },
  margin: { b: 0, l: 0, r: 0, t: 0 },
  autosize: true,
};

Note: to do the mapping, I simply iteratively looked for the best zoom level given the circle at different values (30,25,20…) and I performed a linear interpolation for the values in between.

Adding a title above the table

This is a little change to the app but it brings more clarity to the end user. When a request is made, we want a title to show explicitly the location of the research and the type of fuel.

The API call provides already the city information and the gas type can be updated when a click is made on the button to make a new request.

We are going to pass very quickly on the modifications to make as it is mostly reusing what we did until now with updating/passing state variables from one component to another.

In App.js, we create our two new state variables

const [citySearch, setCitySearch] = useState("");
const [gasTypeSearch, setGasTypeSearch] = useState("");

We can then update the ApiCallAndUpdateState function which is used when a user clicks a button.

const ApiCallAndUpdateState = (gasType, postalCode) => {
  fetch(`${APIURL}/stations?oil_type=${gasType}&postal_code=${postalCode}`)
    .then((res) => {
      return res.json();
    })
    .then((data) => {
      ...
      setGasTypeSearch(gasType);
      setCitySearch(data["city"]);
    });
};

We can now simply add a header between the StationsFilter and StationsTable:

  

{citySearch} - {gasTypeSearch}

This now results in a header appearing above the table showing the current location of the research, as wanted.

Consistent colors in the maps

In plotly, not precise lower and upper bounds when defining a heatmap will result by default stretching the scale to the minimum and maximum values of the population.

This has a major disadvantage: In the case of a station that prices really lower or really higher than average, the color scale will not have a relevant meaning anymore.

Another problem we might meet is that the stations' colors will change depending on the distance used to filter because the number of stations will change, so will the max/min prices, and so, ultimately, the scale.

To avoid the issue, we need to decide on a minimum and maximum value. Given the divergence color map used, we would like also the bounds to be symmetric. There are many possible solutions to that problem, I personally decided to go on "Price average in the 30km radius +/- 10%", assuming that prices above or below that 10% around the average are anyway too high or too low.

To compute the average, we need to take the non-filtered data and calculate the sum over length. This is done in Javascript using the reduce method:

  const sumPrice = stationsData.reduce(
    (total, value) => total + value["price_per_L"],
    0
  );

  const avgPrice = sumPrice / stationsData.length;

Note that sumPrice and avgPrice don't need to be state variables: they will be always calculated when the new stationsData is updated.

We can pass now avgPrice to StationsMap via the props as usual:

And update the chart by thresholding the prices above or below the defined bound.

const COLOR_PRICE_THRESHOLD = 0.1
const stationsPriceColor = {
    marker: {
      color: props.stationsData.map((e) => {
        var price = e["price_per_L"];
        if (price > props.avgPrice * (1+COLOR_PRICE_THRESHOLD)) {
          price = props.avgPrice * (1+COLOR_PRICE_THRESHOLD);
        }
        if (price < props.avgPrice * (1-COLOR_PRICE_THRESHOLD)) {
          price = props.avgPrice * (1-COLOR_PRICE_THRESHOLD);
        }
        return price;
      }),
      ...
    },
    ...,
  };

The next figure illustrates the difference between standard bounds and our custom bounds, which brings more contrast:

Cleaning hover information in the chart

Part of the small details to fix, the default hover from plotly needs a bit of customization. There are currently two main problems:

  • The hover event triggered when a user checks a station shows also the lat/lon by default, which is not something we want to see (we would like to see only the price)
  • When the mousse passes on the black trace or the red dot, coordinates are also displayed and we would like to remove this interaction

For the first point, we can simply pass a hovertemplate parameter that controls the content displayed when there is a mousse hover. The below formula includes only the parameter in the "text" attribute and remove the default box.

 hovertemplate: '%{text}'

For the second point, it is even easier, we just need to pass a parameter

hoverinfo: "skip"

which will completely remove the hover interaction for the trace.

Handle API errors

There is a last thing we will explore in this article: handling an API error. If a user tries a postal code that is not in the database, the API is returning a custom error 400. When this happens, we want to modify our text field to make the user understand there was a problem with his input and highlight the input field in red.

Let's start by adding a new state variable in App.js

const [apiError, setApiError] = useState(false);

We can now modify our fetch method to include an action when something abnormal happens.

The first step is to throw an error if we don't receive a proper response from the API.

fetch(`${APIURL}/stations?oil_type=${gasType}&postal_code=${postalCode}`)
  .then((res) => {
    if (!res.ok) {
      throw new Error("Problem with the API...");
    }
    return res.json();
  })
  .then((data) => {...})

After that, we need to catch this error at the end of our processing pipeline, and setApiError to "true". We need also to make sure that apiError is set back to "false" after a good request:

fetch(`${APIURL}/stations?oil_type=${gasType}&postal_code=${postalCode}`)
  .then((res) => {...})
  .then((data) => {
    ...
    setApiError(false);
  })
  .catch((error) => {
    setApiError(true);
  });

We can finally pass apiError to our StationsForm component and modify the styling of the postalCode text field. One way of doing it is to add an extra class to the text field when apiError is "true"…

… and add extra CSS for that input-error class

.input-error {
  border: solid 2px red;
}


Conclusion

This last implementation concludes completely the chapter dedicated to the development of the UI of a web app using React.

React is gaining more and more traction in the data scientist/data analyst community, and even if there is certainly a lot of time to invest in learning the framework, we saw through this example that once mastered, this allows us to build robust, powerful, and responsive applications.

With the UI completed, are now halfway to the completion of the prototype phase. In the next article, we are going to talk about the server side and prepare our API and our database.

Tags: Data Science Geospatial Hands On Tutorials Maps React

Comment