0

I built a boat visualizer using this API. After inquiring the API I am able to obtain a json response with the vessels I am interested in and write those API information into a MongoDB database. API request can be done every 1 minute, and that is why I used const NodeCache = require('node-cache'); module to by-pass the 1-minute limit.

The problem: Everything seems to be working well, but if I manually refresh the page to see the updated position of the vessels and send a request before 1 minute I get a 304 of Unhandled promise rejection. So the program does not crash, but skip constantly 1 minute in writing the information to MongoDB. Meaning that I get the position every 2 minutes because of the refresh page action instead of every 1 minute. Why is that happening?

UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)

error

Typical answer from the API is the following below:

[  
    {  
        "AIS":{  
            "MMSI":227441980,
            "TIMESTAMP":"2017-08-11 11:17:37 UTC",
            "LATITUDE":46.1459,
            "LONGITUDE":-1.16631,
            "COURSE":360.0,
            "SPEED":0.0,
            "HEADING":511,
            "NAVSTAT":1,            
            "IMO":0,
            "NAME":"CLEMENTINE",
            "CALLSIGN":"FJVK",
            "TYPE":60,
            "A":0,
            "B":0,
            "C":0,
            "D":0,
            "DRAUGHT":0.0,
            "DESTINATION":"",
            "ETA_AIS":"00-00 00:00",
            "ETA":"",
            "SRC":"TER",
            "ZONE": "North Sea",
            "ECA": true      
        }
    }
]

Below the most important part of the code:

server

var express = require('express');
var router = express.Router();
var axios = require('axios');
const NodeCache = require('node-cache');
const myCache = new NodeCache();

let hitCount = 0;

/* GET home page. */
router.get('/', function(req, res, next) {
    res.render('index', { title: 'Express' });
});

router.get('/hello', async function(req, res, next) {
    const allData = myCache.get('allData');

    if (!allData) {
        hitCount++;
        try {
            const { data } = await axios.get(
                'https://api.vesselfinder.com/vesselslist?userkey=KEY'
            );
            const { metaData, ships } = data;
            myCache.set('allData', data, 70);
            console.log(data + 'This is the data');
            res.send(data);
        } catch (error) {
            res.send(error);
            console.log(error);
        }
    }
    res.send(allData);
});

module.exports = router;

client

class BoatMap extends Component {
    constructor(props) {
        super(props);
        this.state = {
            // .........  
        };
        this.updateRequest = this.updateRequest.bind(this);
    }

    async componentDidMount() {
        this.countDownInterval = setInterval(() => {
        }, 500);

        await this.updateRequest();

        this.updateInterval = setInterval(() => {
            this.updateRequest();
        }, 60 * 1000);
    }


    async updateRequest() {
        const url = 'http://localhost:3001/hello';
        const fetchingData = await fetch(url);
        const ships = await fetchingData.json();
        console.log('fetched ships', ships);

        if (JSON.stringify(ships) !== '{}') {
            if (this.previousTimeStamp === null) {
                this.previousTimeStamp = ships.reduce(function(obj, ship) {   
                    obj[ship.AIS.NAME] = ship.AIS.TIMESTAMP;
                    return obj;
                }, {});
            }

            this.setState({
                ships: ships,
                filteredShips: ships
            });

            this.props.callbackFromParent(ships);

            for (let ship of ships) {
                if (this.previousTimeStamp !== null) {
                    if (this.previousTimeStamp[ship.AIS.NAME] === ship.AIS.TIMESTAMP) {
                        this.previousTimeStamp[ship.AIS.NAME] = ship.AIS.TIMESTAMP;
                        console.log('Same timestamp: ', ship.AIS.NAME, ship.AIS.TIMESTAMP);
                        continue;
                    } else {
                        this.previousTimeStamp[ship.AIS.NAME] = ship.AIS.TIMESTAMP;
                    }
                }

                let _ship = {
                    // ships data ....
                };
                const requestOptions = {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify(_ship)
                };
                await fetch('http://localhost:3001/users/vessles/map/latlng', requestOptions);
            }
        }
    }
}





render() {
    const noHoverOnShip = this.state.hoverOnActiveShip === null;
    // console.log("color", this.state.trajectoryColor);
    return (
        <div className="google-map">
            <GoogleMapReact
                // ships={this.state.ships}
                bootstrapURLKeys={{ key: 'key' }}
                center={{
                    lat: this.props.activeShip ? this.props.activeShip.latitude : 37.99,
                    lng: this.props.activeShip ? this.props.activeShip.longitude : -97.31
                }}
                zoom={5.5}
                onGoogleApiLoaded={({ map, maps }) => {
                    this.map = map;
                    this.maps = maps;
                    // we need this setState to force the first mapcontrol render
                    this.setState({ mapControlShouldRender: true, mapLoaded: true });
                }}
            >
                {this.state.mapLoaded ? (
                    <div>
                        <Polyline
                            map={this.map}
                            maps={this.maps}
                            markers={this.state.trajectoryData}
                            lineColor={this.state.trajectoryColor}
                        />
                    </div>
                ) : (
                    ''
                )}

                {/* Rendering all the markers here */}
                {this.state.filteredShips.map((ship) => (
                    <Ship
                        ship={ship}
                        key={ship.AIS.MMSI}
                        lat={ship.AIS.LATITUDE}
                        lng={ship.AIS.LONGITUDE}
                        logoMap={this.state.logoMap}
                        logoClick={this.handleMarkerClick}
                        logoHoverOn={this.handleMarkerHoverOnShip}
                        logoHoverOff={this.handleMarkerHoverOffInfoWin}
                    />
                ))}

What I have done so far:

1) I also came across this source to help me solve the problem but no luck.

2) Also I consulted this other source, and also this one but both of them did not help me to figure out what the problem might be.

3) I dug more into the problem and found this source too.

4) I read this one too. However, neither of these has helped me fix the problem.

5) I also found this source very useful but still no solution.

Thank you very much for pointing to the right direction for solving this issue.

Emanuele
  • 2,194
  • 6
  • 32
  • 71
  • You should catch errors from the `this.updateRequest()` in your `setInterval`; the error should contain a call stack so you know which line the error is on. – Jacob Jun 16 '20 at 17:45
  • @Jacob, thanks for your suggestion. Could you please integrate your answer with some code so that I understand your suggestion? :) – Emanuele Jun 16 '20 at 18:09
  • We still don't know how to answer your question until we know what the actual error is. A simple way to get more data is to replace `this.updateRequest()` with `this.updateRequest().catch(console.error)` so that the error is no-longer an unhandled rejection, and then look at your error console to see what the error is and what the line number is. Or perhaps you already have that in your console and you can add to the question. – Jacob Jun 16 '20 at 18:17
  • Never mind, I see in the comments of the answer that the error is `this.state.filteredShips.map is not a function`, but your code as posted doesn't have that line. Are you missing the code that is throwing the error? – Jacob Jun 16 '20 at 18:20
  • I updated the question with the print screen if that could be useful. – Emanuele Jun 16 '20 at 18:24
  • I also added the `render` function on the client side if that could help – Emanuele Jun 16 '20 at 18:29
  • You need to handle the case where `this.state.filteredShips` is not yet populated, since that loads asynchronously. Render something like a loading indicator. – Jacob Jun 16 '20 at 19:31

2 Answers2

1

As the error says, this.state.filteredShips.map in some cases is not a function. This probably means that your initial state that you omitted does not have this.state.filteredShips as an Array. That's ok if you don't yet have filtered ships, but your rendering code has to take into account all possibilities of your props and state. If you're calling this.state.filteredShips.map, and that isn't a function, your rendering is going to fail. You should check the value of this.state.filteredShips and render something else for the case where it's not yet populated, something like:

{Array.isArray(this.state.filteredShips)
  ? this.state.filteredShips.map((ship) => (
    <Ship
      ship={ship}
      key={ship.AIS.MMSI}
      lat={ship.AIS.LATITUDE}
      lng={ship.AIS.LONGITUDE}
      logoMap={this.state.logoMap}
      logoClick={this.handleMarkerClick}
      logoHoverOn={this.handleMarkerHoverOnShip}
      logoHoverOff={this.handleMarkerHoverOffInfoWin}
    />
  ))
  : 'Loading...'
}
Jacob
  • 77,566
  • 24
  • 149
  • 228
  • Hi Jacob and thanks for your help :). I tried it and it was working for a much longer period now. I see when you said "take into account all possibilities". :) – Emanuele Jun 17 '20 at 13:46
  • I still have an strange error triggering and was wondering if you have time to take a look? The [question is here](https://stackoverflow.com/questions/62430578/reactjs-throwing-typeerror-cannot-read-property-latlng-of-undefined). Thank you for your time yesterday! Very much appreciated! :) – Emanuele Jun 17 '20 at 13:49
0

I understand you want to have the state cmd:

       this.setState({
            ships: ships,
            filteredShips: ships
        });

done before going to the

for (let ship of ships) {...

If this is the case you need to use the setState callback:

           this.setState({ ..... }, () => {do what you need next})

setState is asyncronuous and unless you use the call back there is no way to warrant that the state vars ships and filteredShips will get the values you need before going to the loop.

On the other hand, I never use await for calls to a server. If I use Axios I tend to use its call backs. More info at:

https://www.npmjs.com/package/react-axios

Hope this helps.

Jose Cabrera Zuniga
  • 2,348
  • 3
  • 31
  • 56
  • Thanks for stopping by and reading the question. I see what you are saying and that actually explains why I get `TypeError: this.state.filteredShips.map is not a function` – Emanuele Jun 16 '20 at 18:05
  • Could you please integrate your answer with the code that I posted so that I can fully understand your suggestion? – Emanuele Jun 16 '20 at 18:06
  • I'll suggest you to first make the setState to work as needed. Create a function that will run the code you need executed after the setState is done and call this function from within the setState Call back. You are using fetch but I use Axios. – Jose Cabrera Zuniga Jun 16 '20 at 18:14