2

I built a boat visualizer using specific API. The API returns a json response that I injecting it in a table.

The problem: Sometimes during the day I noticed that the application would stop working throwing an instance of:

Unhandled Rejection (TypeError): ships.reduce is not a function

Below for completeness the print screen of the error:

err

Below the code I am using:

const ShipTracker = ({ ships, setActiveShip }) => {
  console.log("These are the ships: ", { ships });

  return (
    <div className="ship-tracker">
      <Table className="flags-table" responsive hover>
        <thead>
          <tr>
            <th>#</th>
            <th>MMSI</th>
            <th>TIMESTAMP</th>
            <th>LATITUDE</th>
            <th>LONGITUDE</th>
            <th>COURSE</th>
            <th>SPEED</th>
            <th>HEADING</th>
            <th>NAVSTAT</th>
            <th>IMO</th>
            <th>NAME</th>
            <th>CALLSIGN</th>
          </tr>
        </thead>
        <tbody>
          {ships.map((ship, index) => {
            // <-- Error Here
            const {
              MMSI,
              TIMESTAMP,
              LATITUDE,
              LONGITUDE,
              COURSE,
              SPEED,
              HEADING,
              NAVSTAT,
              IMO,
              NAME,
              CALLSIGN
            } = ship.AIS;

            const cells = [
              MMSI,
              TIMESTAMP,
              LATITUDE,
              LONGITUDE,
              COURSE,
              SPEED,
              HEADING,
              NAVSTAT,
              IMO,
              NAME,
              CALLSIGN
            ];

            return (
              <tr
                onClick={() =>
                  setActiveShip(
                    ship.AIS.NAME,
                    ship.AIS.LATITUDE,
                    ship.AIS.LONGITUDE
                  )
                }
                key={index}
              >
                <th scope="row">{index}</th>
                {cells.map(cell => (
                  <td key={ship.AIS.MMSI}>{cell}</td>
                ))}
              </tr>
            );
          })}
        </tbody>
      </Table>
    </div>
  );
};

Googlemap.js

class BoatMap extends Component {
  constructor(props) {
    super(props);
    this.state = {
      ships: [],
      filteredShips: [],
      type: "All",
      shipTypes: [],
      activeShipTypes: []
    };
    this.updateRequest = this.updateRequest.bind(this);
    this.countDownInterval = null;
    this.updateInterval = null;
    this.map = null;
    this.maps = null;
    this.previousTimeStamp = null;
  }

  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 = {
          // ship data ...
        };
        const requestOptions = {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(_ship)
        };
        await fetch(
          "http://localhost:3001/users/vessles/map/latlng",
          requestOptions
        );
        // console.log('Post', Date());
      }
    }
  }

  render() {
    const noHoverOnShip = this.state.hoverOnActiveShip === null;
    return (
      <div className="google-map">
        <GoogleMapReact
          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>
          )}

          {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}
              />
            ))
          ) : (
            <div />
          )}
        </GoogleMapReact>
      </div>
    );
  }
}

export default class GoogleMap extends React.Component {
  state = {
    ships: [],
    activeShipTypes: [],
    activeCompanies: [],
    activeShip: null,
    shipFromDatabase: []
  };

  setActiveShip = (name, latitude, longitude) => {
    this.setState({
      activeShip: {
        name,
        latitude,
        longitude
      }
    });
  };

  setShipDatabase = ships => {
    this.setState({ shipFromDatabase: ships });
  };

  // passing data from children to parent
  callbackFromParent = ships => {
    this.setState({ ships });
  };

  render() {
    return (
      <MapContainer>
        {/* This is the Google Map Tracking Page */}
        <pre>{JSON.stringify(this.state.activeShip, null, 2)}</pre>
        <BoatMap
          setActiveShip={this.setActiveShip}
          activeShip={this.state.activeShip}
          handleDropdownChange={this.handleDropdownChange}
          callbackFromParent={this.callbackFromParent}
          shipFromDatabase={this.state.shipFromDatabase}
          renderMyDropDown={this.state.renderMyDropDown}
          // activeWindow={this.setActiveWindow}
        />
        <ShipTracker
          ships={this.state.ships}
          setActiveShip={this.setActiveShip}
          onMarkerClick={this.handleMarkerClick}
        />
      </MapContainer>
    );
  }
}

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.

Thanls for pointing to the right direction for solving this problem.

goto
  • 4,336
  • 15
  • 20
Emanuele
  • 2,194
  • 6
  • 32
  • 71
  • Can you post the code where you render your ShipTracker component, please? also how does that ships prop is getting set – ludwiguer Jun 17 '20 at 22:46
  • 1
    This means you're trying to use `Array` methods on an object that is not an array. If this data is coming from an `API`, where you "always" expect it to be in an array form, but sometimes (perhaps the request fails) doesn't give you the right data, then you should handle for that. – goto Jun 17 '20 at 22:47
  • Since you have the error from time to time, I guess `ships.map` could be `undefined`. I guess you should try finding the type of `ships` and `ships.map` (or `ships.reduce`) when the error occurs. (You could `try...catch`, with a print of the types in the catch, and then throw back your exception to prevent more code being executed. If you don't have access to the logs, you can also throw a new exception containing the types of `ships` and `ships.map`.) – Naeio Jun 17 '20 at 22:54
  • @ludwiguer, Hi and thanks so much for stopping by and reading the question. I posted part of the file **googlemap.js** that is where I render the component `ShipTracker` if that is more useful to understand why it fails – Emanuele Jun 17 '20 at 23:01
  • Thank you for all your responses so far! Your help is very very useful! :) – Emanuele Jun 17 '20 at 23:01
  • @goto1, thank you very much for your suggestion. Yes the API returns a certain number of ships (basically an array of ships). How could I check what you said "sometimes (perhaps the request fails) doesn't give you the right data, then you should handle for that.". That is a good advice do you have any suggestions I could apply ? – Emanuele Jun 17 '20 at 23:11

3 Answers3

1

This won't solve why your ships prop is not always an array, but it will help you to protect your code against this unhandled exception

<tbody>
   {Array.isArray(ships) && ships.map((ship, index) => {
      const {
        MMSI,
        // rest of your code here
ludwiguer
  • 2,177
  • 2
  • 15
  • 21
  • I have not come across this method before but I suppose perhaps this could be considered more elegant than mine, thanks for sharing – Max Carroll Jun 17 '20 at 22:52
  • Hi and thanks so much for stoppingby and reading the question. I posted part of the file **googlemap.js** that is where I render the component `ShipTracker` if that is more useful to understand why it fails – Emanuele Jun 17 '20 at 23:01
  • You can start by checking that this line is logging, in fact, an array `console.log('fetched ships', ships);`? and then go down your components tree to detect the problem, but either way, since the API can change and return a different thing or fail you should protect your code using the snippet above. – ludwiguer Jun 17 '20 at 23:17
1

One way you could go about this is to default to an empty array if something goes wrong with your fetch request inside updateRequest:

async updateRequest() {
  const url = "http://localhost:3001/hello";
  const defaultValue = [];
  const ships = await fetchShips(url, defaultValue);

  // safe to use `Array` methods on an empty `array`
  if (this.previousTimeStamp === null) {
    this.previousTimestamp = ships.reduce(...);
  }
}

function fetchShips(url, defaultValue) {
  return fetch(url)
    .then(response => {
      if (!response.ok) {
        throw Error(response.statusText);
      }
      return response.json();
    })
    .then(data => {
      if (Array.isArray(data)) {
        return data;
      }
      // return the default value (empty array)
      // so that your application doesn't crash 
      return defaultValue;
    })
    .catch(error => {
      console.error(error.message);

      // catch other errors and return the default
      // value (empty array), so that your application doesn't crash
      return defaultValue;
    });
}

However, you should handle errors appropriately and show a message that says something went wrong for a better user experience rather than not showing anything at all.

async updateRequest() {
  const url = "http://localhost:3001/hello";
  const defaultValue = [];
  const ships = await fetchShips(url, defaultValue).catch(e => e);

  if (ships instanceof Error) {
    // handle errors appropriately
    return;
  }
  // otherwise continue, with an empty array still a
  // possibility, but won't break the app
  if (this.previousTimeStamp === null) {
    this.previousTimestamp = ships.reduce(...);
  }
}

function fetchShips(url, defaultValue) {
  return fetch(url)
    .then(response => {
      if (!response.ok) {
        throw Error(response.statusText);
      }
      return response.json();
    })
    .then(data => {
      if (Array.isArray(data)) {
        return data;
      }
      // return the default value (empty array)
      // so that your application doesn't crash 
      return defaultValue;
    });
}
goto
  • 4,336
  • 15
  • 20
  • Thanks for your time in answering my question. This is exactly what I need. I will implement your suggestion and get back to you! Again I really appreciate your time in helping me! – Emanuele Jun 18 '20 at 01:43
0

This is probably occurring because ships is not an array at the time reduce is being called, perhaps its null?

If ships is state on the parent component being passed down then perhaps it is initially null and then gets updates, so the first time the component is rendered its calling reduce on null?

If the version of JavaScript you are using supports null propagation you can use

ships?.reduce so that the first time its rendered if its null it won't try to invoke the reduce function on null, but then when it renders and it is, all should be fine, this is quite a common pattern.

If your JavaScript version doesn't support null propagation you can use

ships && ships.length > 0 && ships.reduce(...

so you should also change

{ships.map((ship, index) => { // <-- Error Here to


{
   ships?.map((ship, index) => 
   ...

   // or

  ships && ships.length > 0 && ships.map((ship, index) => 
  ....
}
Max Carroll
  • 4,441
  • 2
  • 31
  • 31
  • 1
    `ships && ships.length > 0 && ships.map((ship, index) =>` will die if ships is a string – ludwiguer Jun 17 '20 at 22:53
  • yes, I use this pattern in my code, when I'm certain its going to be an array, I think your solution is better I just haven't seen it before, I may introduce this into my code. I'm thinking of going the typescript route also – Max Carroll Jun 17 '20 at 22:55
  • @MaxCarroll `TypeScript` wouldn't help you here at all if this was coming from an `API` request where you are not guaranteed that whatever you get is going to be an `Array` - you should still handle those cases properly. – goto Jun 17 '20 at 22:58
  • Doesn't Typescript force you to know whether something could to be null or not though, so I would be able to guarantee by the time it was rendered that it wouldn't be null? – Max Carroll Jun 17 '20 at 22:59
  • You don't ship `TypeScript` to a browser, you ship regular `JavaScript`. Unless there's a feature inside `JavaScript` that does what you say, `TypeScript` won't do anything about `API` calls that are supposed to return `array`s but all of the sudden return an empty `string`. – goto Jun 17 '20 at 23:03
  • If my I have an api call to the server in TypeScript I can enforce a type on it e.g. `Array` however if its null I will get an error at runtime because if its null then I should actually use the type `Array | null ` so I can use typescript to enforce that a call to the API results in an empty array rather than null, and you can call map, reduce etc... other array functions on an empty array – Max Carroll Jun 17 '20 at 23:05
  • @MaxCarroll give this a try and let me know how it goes. Create a such function, transpile it to something that browsers can understand, create an API endpoint that returns an array, then change that API endpoint to return something like a `string`, and let me know if your function fails or not. – goto Jun 17 '20 at 23:08
  • @MaxCarroll that's fine too, give it a try and let me know if it fails or not. – goto Jun 17 '20 at 23:10
  • Okay yes so you would get a runtime error, but not compilation errors, I didn't realise you were talking about runtime errors. I don't really want to continue this argument because my basic point is that type safety provided by TypeScript is really useful and may prevent similar issues, as opposed to it being a magic bullet which will stop all runtime errors – Max Carroll Jun 17 '20 at 23:11
  • @MaxCarroll yes, which goes back to my original point, `TypeScript` wouldn't help you here at all - you still need to handle such cases properly. – goto Jun 17 '20 at 23:12
  • @MaxCarroll, thanks so much for stopping by and reading the question. I posted part of the file **googlemap.js** that is where I render the component `ShipTracker` if that is more useful to understand why it fails :) – Emanuele Jun 17 '20 at 23:13
  • I was never rely trying to imply that TypeScript would save us from arrays being strings but it can allow us to know at compile time whether things should or shouldn't be null – Max Carroll Jun 17 '20 at 23:16
  • @MaxCarroll yeah read my first comment again - I don't care about "at compile time", I care what's running in a browser and what could go wrong. – goto Jun 17 '20 at 23:19
  • Yes I suppose that's whats really important, we can fabricate a washing machine, but then we can still put a brick in it, why do we care how well the washing machine was built if we can still put a brick in it – Max Carroll Jun 17 '20 at 23:21