Reputation: 31
I am building a web application that is consuming a public API and displaying that data into a table. But half the time when I click the "Click to Display Hottest Planet" button it breaks and shows this error. How can solve this without having to worry about it breaking. I want to deploy it as a portfolio project. It works fine when tour is not active.
TypeError: hotPlanet is undefined
103 | {popup && <Popup
104 | boilerPlate='The Hottest Planet is : '
> 105 | content={hotPlanet.PlanetIdentifier}
| ^ 106 | handleClose={togglePopup}
107 | />}
108 | <Table className="table-component" data={planets} columns={COLUMNS}></Table>
Homepage.js file
import React,{useState,useEffect,useContext} from 'react'
import axios from "axios";
import Table from '../components/Table'
import {COLUMNS} from '../components/columns';
import './styles.css';
import Tour from 'reactour'
import getTopN from '../components/FindMax';
import getMax from '../components/WayTwo';
import Popup from '../components/Popup';
const steps = [
{
selector: '.Welcome',
content: 'This is my solution to take home assignment',
},
{
selector: '.reload-btn',
content: ' This buttons reloads the data if need be'
},
{
selector: '.btn-tutorial',
content: 'These buttons right next to table heading help sort the data. If the button is white it is unsorted, if the button in red then it is sorted in descending order. If it is green it is sorted in ascending order'
},
{
selector: '.sol1',
content: 'To find out about orphan planets Go to the dropdown next to Stellar Formation column and select 3'
},
{
selector: '.sol2',
content: 'To group the planets by the year they where discovered please type in the input box next to " Discovered In"'
},
{
selector: '.sol3',
content: 'To Find the Hottest star Press the dot button next to Host Temperature column'
}
];
export default function HomePage() {
const [planets,setPlanets] = useState([]);
const [reload,setReload] = useState(0);
const [isTourOpen, setIsTourOpen] = useState(true);
const [popup,setPopup] = useState(false);
const [hotPlanet,setHotPlanet] = useState({})
function getData()
{
const url = "https://gist.githubusercontent.com/joelbirchler/66cf8045fcbb6515557347c05d789b4a/raw/9a196385b44d4288431eef74896c0512bad3defe/exoplanets";
axios.get(url).then((response) => setPlanets(response.data));
}
function newHotPlanet()
{
var maxPpg = getMax(planets, "HostStarTempK");
setHotPlanet(maxPpg);
}
useEffect(()=>
{
getData();
console.log(planets)
newHotPlanet();
},[reload])
useEffect(()=>
{
getData();
console.log(planets)
newHotPlanet();
},[])
function handleReload(e)
{
getData();
setReload(reload+1)
}
const [isOpen, setIsOpen] = useState(false);
const togglePopup = () => {
setPopup(!popup);
}
function handlePopup(e)
{
e.preventDefault();
getData();
var maxPpg = getMax(planets, "HostStarTempK");
setHotPlanet(maxPpg);
console.log(hotPlanet);
setPopup(true);
}
return (
<div>
<h1 className = "Welcome">Planet Portal</h1>
<Tour
steps={steps}
isOpen={isTourOpen}
onRequestClose={() => setIsTourOpen(false)}
/>
<button className="reload-btn" onClick={handleReload}> Reload Data</button>
<button onClick={togglePopup} className="sol3"> Click to Display Hottest Planet</button>
{popup && <Popup
boilerPlate='The Hottest Planet is : '
content={hotPlanet.PlanetIdentifier}
handleClose={togglePopup}
/>}
<Table className="table-component" data={planets} columns={COLUMNS}></Table>
</div>
)
}
It is working fine when I am not using Tour from react tour
Upvotes: 1
Views: 254
Reputation: 23753
The reason here is that you don't handle async data loading properly:
function handlePopup(e)
{
e.preventDefault();
getData();
var maxPpg = getMax(planets, "HostStarTempK");
setHotPlanet(maxPpg);
console.log(hotPlanet);
setPopup(true);
}
where line getData()
just start loading. Data is not fetched yet, and definitely is not set to planets
. And without waiting next line var maxPpg = ...
tries to find the best item. And function getMax()
returns undefined
for empty array of planets
. That's how hotPlanet
becomes undefined
and later crashes on re-render.
The easiest patch would be to await till data is fetched. First we need to return Promise
from getData
:
function getData()
{
const url = // .....;
return axios.get(url).then((response) => setPlanets(response.data));
}
And next we await for it to be finished in handlePopup
:
async function handlePopup(e)
{
e.preventDefault();
await getData();
var maxPpg = getMax(planets, "HostStarTempK");
setHotPlanet(maxPpg);
setPopup(true);
}
Even for other approaches we still need await getData()
so popup will never render while hotPlanet
is not calculated yet.
Take a look into AJAX and APIs section.
But easiest approach is not the best. To my opinion we always want to have hottest planet to be actual. So instead of calculating it on click, we may always recalculate(solution #2) hottest on any change in planets
:
useEffect(() => {
setHotPlanet(getMax(planets, "HostStartTempK"));
}, [planets]);
But there is even better approach(solution #3). Data that is calculated by other state is called "derived". You actually may not need to keep hotPlanet
in separate variable but just recalculate value:
{popup && <Popup
boilerPlate='The Hottest Planet is : '
content={getMax(planets, "HostStarTempK").PlanetIdentifier}
handleClose={togglePopup}
/>}
Yes, on complex search logic or huge arrays(I consider 100 elements list to be small sized) there might be performance issue. In this case we may want to memoize hotPlanet
(soltion #4) that still will be more suitable than having additional state:
const hotPlannet = useMemo(() => getMax(planets, "HostStarTempK"), [planets]);
Upvotes: 1