React ref.current is still null in componentDidUpdate - reactjs

I'm trying to zoom-in on a set of coordinates using react-map-gl. To do so, the library encourages us to use React's Refs. When instantiating the component in render, I'm passing down a ref that should hold the underlying MapBox map object. I managed to make the behavior work. Only problem: it's highly inconsistent.
I'm calling a method called setCamera in componentDidUpdate. But it only works on the initial load. If I reload the page, I've got an error from Gatsby. If I close the error it will work again. Up until the next reload.
The error is that this.mapRef.current is null. I tried to put a conditional to verify that the value isn't null before trying to access it, however this cause the animation to just never work. No matter how many times I reload, it will never be performed. Whereas without the conditional it could at least work half the time before crashing. So this already is a mystery in itself and if someone has an idea for why such behavior can happen, I'm all ears.
Still, I wasn't discouraged and tried to put the call to setCamera in a setTimeout and yes, it works! Even putting a very low timeout like 1 makes the code work 95% of the time. But I'm unsatisfied with it, because I understand that putting that kind of timers isn't what I'm supposed to do and to make things worse, it doesn't fix the issue consistently.
My understanding of the problem is that MapRef is still not set in componentDidUpdate for some reason. It's being set sometimes later. I don't know if React supports threading or if some kind of async witchcraft is deceiving me behind the scenes, but what I'm wondering is when am I guaranteed for my ref to be properly set? Where or how should I write this code?
Thank you in advance to anyone who can help me on that. 🙂
Here's my sample code:
import React, {Component} from 'react';
import MapGL, {NavigationControl, Source, Layer} from 'react-map-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import gpxParser from 'gpxparser';
import bbox from '#turf/bbox';
const MAPBOX_TOKEN = 'exampleToken'
class HikeMapbox extends Component {
constructor(props) {
super(props)
this.state = {
gpxData: '',
};
}
// Read et get the GPX file, return it as a bunch of text
componentDidMount() {
const gpxPath = `https:${this.props.gpxPath}`;
// Simple GET request using fetch
fetch(gpxPath)
.then(response => response.text())
.then(text => this.setState({ gpxData: text }));
}
componentDidUpdate() {
// Set the camera on didUpdate
setTimeout(() => {
const geoJsonData = this.getGeoJson();
this.setCamera(geoJsonData);
}, 10);
}
// Get Max and Min Lat and Lon to Zoom on the GPX trace
setCamera(geoJsonData) {
if (geoJsonData) {
const [minLng, minLat, maxLng, maxLat] = bbox(geoJsonData);
this.mapRef.current.fitBounds(
[
[minLng, minLat],
[maxLng, maxLat]
],
{padding: 40}
);
}
}
// Take the text and parse it as geoJSON
getGeoJson() {
const { gpxData } = this.state;
let gpx = new gpxParser();
try {
gpx.parse(gpxData);
} catch (err) {
console.log(err);
}
const geoJson = gpx.toGeoJSON();
return geoJson
}
// Return the GPX trace in Mapbox
showGpxFile() {
const GeoJsonData = this.getGeoJson();
// Choose the style of the GPX trace
const layerStyle = {
id:'contours',
type:'line',
source:'contours',
paint: {
'line-color': 'blue',
'line-width': 3
}
};
return (
<>
{
// Return the React Mapbox GL code to show the GPX file on the map
GeoJsonData && (
<Source type="geojson" data={GeoJsonData}>
<Layer {...layerStyle} />
</Source>
)
}
</>
)
}
render() {
this.mapRef = React.createRef();
return (
<>
<MapGL
ref={this.mapRef}
initialViewState={{
latitude: this.props.latitude,
longitude: this.props.longitude,
zoom: 8,
}}
style={{width: "100%", height: "100%"}}
mapStyle="mapbox://styles/mapbox/outdoors-v11"
mapboxAccessToken={MAPBOX_TOKEN}
>
<NavigationControl />
{this.showGpxFile()}
</MapGL>
</>
)
}
}
export default HikeMapbox;
By the way, I'm running this code on my computer using gatsby develop. I don't know if that could be related, but I thought it could be relevant.

I've found a solution!
My issue was that setCamera was dependent on two conditions and these two conditions could happen in any order.
Fetch is successful and we have data to display.
The map is loaded and we have a ref to it.
Instead of initializing mapRef, in the constructor or in render, I've made a function…
onMapRefChange = node => {
this.setState({mapRef: node});
// I'm calling my method now
this.setCamera()
};
… That I'm passing in in the ref parameter
<MapGL
ref={this.onMapRefChange}
...
Eventually onMapRefChange will receive an actual map object and then the code in setCamera will be able to access it.

Related

How can one prevent duplicate setTimeout resulting from complex state change?

I apologize for the complexity of the code this question is based around, but it seems the issue itself is arising from the complexity. I wasn't able to replicate the issue with a simpler example. Here is the code repository branch with the issue: https://github.com/chingu-voyages/v42-geckos-team-21/commit/3c20cc55e66e7d0f9122d843222980e404d4910f The left hand (before the change) uses useRef() and does not have the issue, but I don't think useRef() respect's its proper usage.
Here is the main problem code:
import { useState, useRef} from 'react';
import "./Alert.css"
import "animate.css"
import { CSSTransition } from "react-transition-group"
import { time } from 'console';
console.log(typeof CSSTransition);
interface IfcProps {
text: React.ReactNode,
exitAfterDuration: number,
setAlertKey: React.Dispatch<React.SetStateAction<number>>,
alertKey: number
}
const classNames = {
appear: 'animate__bounce',
appearActive: 'animate__bounce',
appearDone: 'animate__bounce',
enter: 'animate__bounce',
enterActive: 'animate__bounce',
enterDone: 'animate__bounce',
exit: 'animate__bounce',
exitActive: 'animate__fadeOut',
exitDone: 'animate__fadeOut'
}
function Alert(props: IfcProps) {
const nodeRef = useRef(null);
let [isIn, setIsIn] = useState(false);
let [previousAlertKey, setPreviousAlertKey] = useState(0);
let [timeoutId, setTimeoutId] = useState<number | null>(null);
// console.log('props', {...props});
console.log('prev, pres', previousAlertKey, props.alertKey)
console.log('state', {isIn, previousAlertKey, timeoutId});
// console.log('prev, current:', previousAlertKey, props.alertKey);
if (props.text === '') {
// do not render if props.text === ''
return null;
} else if (previousAlertKey !== props.alertKey) {
setIsIn(true);
setPreviousAlertKey(oldPreviousAlertKey => {
oldPreviousAlertKey++
return oldPreviousAlertKey;
});
if (timeoutId) {
console.log(timeoutId, 'timeout cleared');
clearTimeout(timeoutId);
}
let localTimeoutId = window.setTimeout(() => {
console.log('executing timeout')
setIsIn(false);
}, props.exitAfterDuration);
console.log({localTimeoutId}, previousAlertKey, props.alertKey);
setTimeoutId(localTimeoutId);
}
return (
<CSSTransition nodeRef={nodeRef} in={isIn} appear={true} timeout={1000} classNames={classNames}>
{/* Using key here to trigger rebounce on alertKey change */}
<div ref={nodeRef} id="alert" className="animate__animated animate__bounce" key={props.alertKey}>
{props.text}
</div>
</CSSTransition>
)
}
export default Alert
Code that resolves issue but probably uses useRef() incorrectly:
import { useState, useRef } from 'react';
import "./Alert.css"
import "animate.css"
import { CSSTransition } from "react-transition-group"
import { time } from 'console';
console.log(typeof CSSTransition);
interface IfcProps {
text: React.ReactNode,
exitAfterDuration: number,
setAlertKey: React.Dispatch<React.SetStateAction<number>>,
alertKey: number
}
const classNames = {
appear: 'animate__bounce',
appearActive: 'animate__bounce',
appearDone: 'animate__bounce',
enter: 'animate__bounce',
enterActive: 'animate__bounce',
enterDone: 'animate__bounce',
exit: 'animate__bounce',
exitActive: 'animate__fadeOut',
exitDone: 'animate__fadeOut'
}
function Alert(props: IfcProps) {
const nodeRef = useRef(null);
const timeoutIdRef = useRef<number | null>(null);
let [isIn, setIsIn] = useState(false);
let [previousAlertKey, setPreviousAlertKey] = useState(0);
console.log({props});
console.log('state', {isIn, previousAlertKey, timeoutIdRef});
console.log('prev, current:', previousAlertKey, props.alertKey);
if (props.text === '') {
// do not render if props.text === ''
return null;
} else if (previousAlertKey !== props.alertKey) {
setIsIn(true);
setPreviousAlertKey(oldPreviousAlertKey => {
oldPreviousAlertKey++
return oldPreviousAlertKey;
});
if (timeoutIdRef.current) {
console.log(timeoutIdRef.current, 'timeout cleared');
clearTimeout(timeoutIdRef.current);
}
let localTimeoutId = window.setTimeout(() => setIsIn(false), props.exitAfterDuration);
console.log({localTimeoutId}, previousAlertKey, props.alertKey);
timeoutIdRef.current = localTimeoutId;
}
return (
<CSSTransition nodeRef={nodeRef} in={isIn} appear={true} timeout={1000} classNames={classNames}>
{/* Using key here to trigger rebounce on alertKey change */}
<div ref={nodeRef} id="alert" className="animate__animated animate__bounce" key={props.alertKey}>
{props.text}
</div>
</CSSTransition>
)
}
export default Alert
The issue shows its head when an invalid row is attempted to be submitted to the database and the Alert component appears. If multiple alerts are triggered in this way, they all disappear when the first setTimeout expires because it was never cleared properly. One timeout should be cleared but because React strict mode renders twice and the creation of a timeout is a side effect, the extra timeout never gets cleared. React isn't aware that there are two timeouts running for every submission attempt (check-mark click).
I'm probably handling my alert component incorrectly, with the alertKey for example.
I feel my problem is related to the fact that my setTimeout is triggered inside the Alert component as opposed to inside the Row component's onClick() handler, as I did so in a simpler example and it did not exhibit the issue.
I fear I may not get any replies as this is a pretty ugly and complex case that requires a fair bit of setup to the dev environment. This may well be a case where I just have to cobble together a solution (e.g. with useRef) and learn the proper React way in the future through experience. Tunnel-vision is one of my faults.
tl;dr Use the dependency array in useHook()
So I took a step back and worked on some other parts of the app, while sometimes doing some research into how others handle a toast notification component, which is what I was effectively working on in the code here. Logrocket's article was helpful: How to create a custom toast component with React.
#Azzy helped put me back on the right track and the article above also uses the useEffect() hook for the timeout.
In my spare time, eventually I came across this article A Simple Explanation of React.useEffect(). The author, Dmitri Pavlutin, finally got it into my head the intended relation of the main component function body and the useEffect hook.
A functional React component uses props and/or state to calculate the
output. If the functional component makes calculations that don't
target the output value, then these calculations are named
side-effects.
. . .
The component rendering and side-effect logic are independent. It
would be a mistake to perform side-effects directly in the body of the
component, which is primarily used to compute the output.
How often the component renders isn't something you can control — if
React wants to render the component, you cannot stop it.
. . .
How to decouple rendering from the side-effect? Welcome useEffect() —
the hook that runs side-effects independently of rendering.
I now present the code that works and is the React way, (or at least is more like the optimal React way than my two previous attempts, see above).
function Alert(props: IfcProps) {
useEffect(() => {
let timeoutId = window.setTimeout(() => setIsIn(false), props.exitAfterDuration);
return function cleanup() {
window.clearTimeout(timeoutId);
}
}, [props.alertKey]);
const nodeRef = useRef(null);
const timeoutIdRef = useRef<number | null>(null);
let [isIn, setIsIn] = useState(false);
let [previousAlertKey, setPreviousAlertKey] = useState(0);
console.log({ props });
console.log('state', { isIn, previousAlertKey, timeoutIdRef });
console.log('prev, current:', previousAlertKey, props.alertKey);
if (props.text === '') {
// do not render if props.text === ''
return null;
} else if (previousAlertKey !== props.alertKey) {
setIsIn(true);
setPreviousAlertKey(oldPreviousAlertKey => {
oldPreviousAlertKey++
return oldPreviousAlertKey;
});
}
return (
<CSSTransition nodeRef={nodeRef} in={isIn} appear={true} timeout={1000} classNames={classNames}>
{/* Using key here to trigger rebounce on alertKey change */}
<div ref={nodeRef} id="alert" className="animate__animated animate__bounce" key={props.alertKey}>
{props.text}
</div>
</CSSTransition>
)
}
export default Alert
To be honest, there's probably a lot of refactoring I can do now that I understand the utility of useEffect(). I likely have other components with side effects dependent on logic dedicated to checking if the current render happened because specific state / props changed. useEffect()'s dependency array is a lot cleaner than conditionals in the function body checking for those state/prop changes.
A lot of the complexity I lamented in my question arose I think because I wasn't properly separating side effects from my main function body.
Thank you for coming to my TED talk. :)

How to set window resize event listener value to React State?

This issue is very simple but I probably overlook very little point. Window screen size is listening by PostLayout component. When window width is less than 768px, I expect that isDesktopSize is false. I tried everything like using arrow function in setIsDesktopSize, using text inside of true or false for state value, using callback method etc... but it's not working.
PostLayout shared below:
import React, {useState,useEffect, useCallback} from 'react'
import LeftSideNavbar from './LeftSideNavbar'
import TopNavbar from './TopNavbar'
export default function PostLayout({children}) {
const [isDesktopSize, setIsDesktopSize] = useState(true)
let autoResize = () => {
console.log("Desktop: " + isDesktopSize);
console.log(window.innerWidth);
if(window.innerWidth < 768 ){
setIsDesktopSize(false)
}else{
setIsDesktopSize(true)
}
}
useEffect(() => {
window.addEventListener('resize', autoResize)
autoResize();
}, [])
return (
<>
<TopNavbar isDesktopSize={isDesktopSize}/>
<main>
<LeftSideNavbar/>
{children}
</main>
</>
)
}
console log is shared below:
Desktop: true
627
This could probably be extracted into a custom hook. There's a few things you'd want to address:
Right now you default the state to true, but when the component loads, that may not be correct. This is probably why you see an incorrect console log on the first execution of the effect. Calculating the initial state to be accurate could save you some jank/double rendering.
You aren't disconnecting the resize listener when the component unmounts, which could result in an error attempting to set state on the component after it has unmounted.
Here's an example of a custom hook that addresses those:
function testIsDesktop() {
if (typeof window === 'undefined') {
return true;
}
return window.innerWidth >= 768;
}
function useIsDesktopSize() {
// Initialize the desktop size to an accurate value on initial state set
const [isDesktopSize, setIsDesktopSize] = useState(testIsDesktop);
useEffect(() => {
if (typeof window === 'undefined') {
return;
}
function autoResize() {
setIsDesktopSize(testIsDesktop());
}
window.addEventListener('resize', autoResize);
// This is likely unnecessary, as the initial state should capture
// the size, however if a resize occurs between initial state set by
// React and before the event listener is attached, this
// will just make sure it captures that.
autoResize();
// Return a function to disconnect the event listener
return () => window.removeEventListener('resize', autoResize);
}, [])
return isDesktopSize;
}
Then to use this, your other component would look like this (assuming your custom hook is just in this same file -- though it may be useful to extract it to a separate file and import it):
import React, { useState } from 'react'
import LeftSideNavbar from './LeftSideNavbar'
import TopNavbar from './TopNavbar'
export default function PostLayout({children}) {
const isDesktopSize = useIsDesktopSize();
return (
<>
<TopNavbar isDesktopSize={isDesktopSize}/>
<main>
<LeftSideNavbar/>
{children}
</main>
</>
)
}
EDIT: I modified this slightly so it should theoretically work with a server-side renderer, which will assume a desktop size.
Try this, you are setting isDesktopSizze to 'mobile', which is === true
const [isDesktopSize, setIsDesktopSize] = useState(true)
let autoResize = () => {
console.log("Desktop: " + isDesktopSize);
console.log(window.innerWidth);
if(window.innerWidth < 768 ){
setIsDesktopSize(true)
}else{
setIsDesktopSize(false)
}
}
I didn't find such a package on npm and I thought it would be nice to create one: https://www.npmjs.com/package/use-device-detect. I think it will help someone :)

Moving slider with Cypress

I've got a Slider component from rc-slider and I need Cypress to set the value of it.
<Slider
min={5000}
max={40000}
step={500}
value={this.state.input.amount}
defaultValue={this.state.input.amount}
className="sliderBorrow"
onChange={(value) => this.updateInput("amount",value)}
data-cy={"input-slider"}
/>
This is my Cypress code:
it.only("Changing slider", () => {
cy.visit("/");
cy.get(".sliderBorrow")
.invoke("val", 23000)
.trigger("change")
.click({ force: true })
});
What I've tried so far does not work.
Starting point of slider is 20000, and after test runs it goes to 22000, no matter what value I pass, any number range.
Looks like it used to work before, How do interact correctly with a range input (slider) in Cypress? but not anymore.
The answer is very and very simple. I found the solution coincidentally pressing enter key for my another test(date picker) and realized that pressing left or right arrow keys works for slider.
You can achieve the same result using props as well. The only thing you need to do is to add this dependency: cypress-react-selector and following instructions here: cypress-react-selector
Example of using {rightarrow}
it("using arrow keys", () => {
cy.visit("localhost:3000");
const currentValue = 20000;
const targetValue = 35000;
const increment = 500;
const steps = (targetValue - currentValue) / increment;
const arrows = '{rightarrow}'.repeat(steps);
cy.get('.rc-slider-handle')
.should('have.attr', 'aria-valuenow', 20000)
.type(arrows)
cy.get('.rc-slider-handle')
.should('have.attr', 'aria-valuenow', 35000)
})
#darkseid's answer helped guide me reach an optimal solution.
There are two steps
Click the slider's circle, to move the current focus on the slider.
Press the keyboard arrow buttons to reach your desired value.
My slider jumps between values on the sliders, therefore this method would work. (I am using Ion range slider)
This method doesn't require any additional depedency.
// Move the focus to slider, by clicking on the slider's circle element
cy.get(".irs-handle.single").click({ multiple: true, force: true });
// Press right arrow two times
cy.get(".irs-handle.single").type(
"{rightarrow}{rightarrow}"
);
You might be able to tackle this using Application actions, provided you are able to modify the app source code slightly.
Application actions give the test a hook into the app that can be used to modify the internal state of the app.
I tested it with a Function component exposing setValue from the useState() hook.
You have used a Class component, so I guess you would expose this.updateInput() instead, something like
if (window.Cypress) {
window.app = { updateInput: this.updateInput };
}
App: index.js
import React, { useState } from 'react';
import { render } from 'react-dom';
import './style.css';
import Slider from 'rc-slider';
import 'rc-slider/assets/index.css';
function App() {
const [value, setValue] = useState(20000);
// Expose the setValue() method so that Cypress can set the app state
if (window.Cypress) {
window.app = { setValue };
}
return (
<div className="App">
<Slider
min={5000}
max={40000}
step={500}
value={value}
defaultValue={value}
className="sliderBorrow"
onChange={val => setValue(val)}
data-cy={"input-slider"}
/>
<div style={{ marginTop: 40 }}><b>Selected Value: </b>{value}</div>
</div>
);
}
render(<App />, document.getElementById('root'));
Test: slider.spec.js
The easiest way I found assert the value in the test is to use the aria-valuenow attribute of the slider handle, but you may have another way of testing that the value has visibly changed on the page.
describe('Slider', () => {
it("Changing slider", () => {
cy.visit("localhost:3000");
cy.get('.rc-slider-handle')
.should('have.attr', 'aria-valuenow', 20000)
cy.window().then(win => {
win.app.setValue(35000);
})
cy.get('.rc-slider-handle')
.should('have.attr', 'aria-valuenow', 35000)
})
})
For whoever comes across this with Material UI/MUI 5+ Sliders:
First off, this github issue and comment might be useful: https://github.com/cypress-io/cypress/issues/1570#issuecomment-606445818.
I tried changing the value by accessing the input with type range that is used underneath in the slider, but for me that did not do the trick.
My solution with MUI 5+ Slider:
<Slider
disabled={false}
step={5}
marks
data-cy="control-percentage"
name="control-percentage"
defaultValue={0}
onChange={(event, newValue) =>
//Handle change
}
/>
What is important here is the enabled marks property. This allowed me to just click straight on the marks in the cypress test, which of course can also be abstracted to a support function.
cy.get('[data-cy=control-percentage]').within(() => {
// index 11 represents 55 in this case, depending on your step setting.
cy.get('span[data-index=11]').click();
});
I got this to work with the popular react-easy-swipe:
cy.get('[data-cy=week-picker-swipe-container]')
.trigger('touchstart', {
touches: [{ pageY: 0, pageX: 0 }]
})
.trigger('touchmove', {
touches: [{ pageY: 0, pageX: -30 }]
})

JSX Element does not have any construct or call signatures

I am trying to add Application Insights in my ReactJS Application. I changed the JS code that is provided on the GitHub Demo to TypeScript.. now I have
class TelemetryProvider extends Component<any, any> {
state = {
initialized: false
};
componentDidMount() {
const { history } = this.props;
const { initialized } = this.state;
const AppInsightsInstrumentationKey = this.props.instrumentationKey;
if (!Boolean(initialized) && Boolean(AppInsightsInstrumentationKey) && Boolean(history)) {
ai.initialize(AppInsightsInstrumentationKey, history);
this.setState({ initialized: true });
}
this.props.after();
}
render() {
const { children } = this.props;
return (
<Fragment>
{children}
</Fragment>
);
}
}
export default withRouter(withAITracking(ai.reactPlugin, TelemetryProvider));
But when I try to import the same component <TelemetryProvider instrumentationKey="INSTRUMENTATION_KEY" after={() => { appInsights = getAppInsights() }}></Telemetry> I get an error Severity Code Description Project File Line Suppression State
(TS) JSX element type 'TelemetryProvider' does not have any construct or call signatures.
I attempted to simply // #ts-ignore, that did not work. How do I go about solving this?
Given the example above, I hit the same issue. I added the following:
let appInsights:any = getAppInsights();
<TelemetryProvider instrumentationKey={yourkeyher} after={() => { appInsights = getAppInsights() }}>after={() => { appInsights = getAppInsights() }}>
Which seem to solve the issue for me, I am now seeing results in Application Insights as expected.
I guess if you want to have the triggers etc on a different Page/Component you may wish to wrap it in your own useHook or just add something like this to the component.
let appInsights:any;
useEffect(() => {
appInsights = getAppInsights();
}, [getAppInsights])
function trackEvent() {
appInsights.trackEvent({ name: 'React - Home Page some event' });
}
Not the best answer, but it's moved me forward. Would be nice to see a simple hooks version in typescript.
Really hope it helps someone or if they have a cleaner answer.

React: setState after a graphQL request

I've been searching for a couple of hours now, but just can't seem to find the answer. See my code below. I'm requesting some metro-information to be used on an info-screen.
I'm getting the information, seeing as console.log works. However I'm having difficulty using this resulting oject. I want to use the data received, so that I can display when the next train arives. To this purpose I try to setState with the result, so that I can access the data-elements further down. However, now I'm stuck at setState giving me problems. I feel that I need to bind the function, but this.main = this.main.bind(this) doesn't work.
import React from "react";
import { GraphQLClient } from "graphql-request";
class Rutetider extends React.Component {
constructor(props) {
super(props);
this.state = {
stoppestedet: "rutetider lastes ned"
};
async function main() {
const endpoint = "https://api.entur.org/journeyplanner/2.0/index/graphql";
const graphQLClient = new GraphQLClient(endpoint, {
headers: {
ET: "lossfelt-tavle"
}
});
const query = `
{
stopPlace(id: "NSR:StopPlace:58249") {
id
name
estimatedCalls(timeRange: 3600, numberOfDepartures: 20) {
realtime
aimedArrivalTime
aimedDepartureTime
expectedArrivalTime
expectedDepartureTime
actualArrivalTime
actualDepartureTime
cancellation
notices {
text
}
situations {
summary {
value
}
}
date
forBoarding
forAlighting
destinationDisplay {
frontText
}
quay {
id
}
serviceJourney {
journeyPattern {
line {
id
name
transportMode
}
}
}
}
}
}
`;
const data = await graphQLClient.request(query);
console.log(data);
this.setState({ stoppestedet: data.stopPlace.name });
}
main().catch(error => console.error(error));
}
render() {
return (
<div>
rutetider
<div className="grid-container2">
<div>Mot byen</div>
<div>fra byen</div>
<div>{this.state.stoppestedet}</div>
</div>
</div>
);
}
}
export default Rutetider;
"probably easier" is to use integrated solution (apollo) than minimal, low level library. In most cases (as project grows), with more components fetching data managing separate GraphQLClient for all of them won't be an optimal solution. Apollo gives you centralised "fetching point", cache .. and many more.
Syntax error comes from function - in class it's enough to write async main()
https://codesandbox.io/s/l25r2kol7q
It probably would be better to save entire data in state and extract needed parts later (at render) and use this object as 'data-ready flag' (as I did for place - 'stoppestedet') - initally undefined (in constructor) for initial render (conditional rendering, some <Loading /> component):
render() {
if (!this.state.stoppestedet) return "rutetider lastes ned";
return (
<div>
rutetider
<div className="grid-container2">
<div>Mot byen</div>
<div>fra byen</div>
<div>{this.renderFetchedDataTable()}</div>
</div>

Resources