2

I have a question of something that looks pretty obvious but It's getting hard for me. I know that for fetching data that will get actually rendered in a component you need to use reacthooks and useState. However I am having a problem because I need to fetch some data and then store it in a variable that it's not part of the component rendering. This is my current code.

import React from 'react'
import { GoogleMap, useJsApiLoader } from '@react-google-maps/api';
import { GoogleMapsOverlay } from "@deck.gl/google-maps";
import {GeoJsonLayer, ArcLayer} from '@deck.gl/layers';
import axios from 'axios';
import {useState} from 'react';

const hasWindow = typeof window !== 'undefined';

function getWindowDimensions() {
  const width = hasWindow ? window.innerWidth : null;
  const height = hasWindow ? window.innerHeight : null;
  return {
    width,
    height,
  };
}

const center = {
  lat: 51.509865,
  lng: -0.118092
};


const deckOverlay = new GoogleMapsOverlay({
  layers: [
    new GeoJsonLayer({
      id: "airports",
      data: markers,
      filled: true,
      pointRadiusMinPixels: 2,
      opacity: 1,
      pointRadiusScale: 2000,
      getRadius: f => 11 - f.properties.scalerank,
      getFillColor: [200, 0, 80, 180],

      pickable: true,
      autoHighlight: true
    }),
    new ArcLayer({
      id: "arcs",
      data: markers,
      dataTransform: d => d.features.filter(f => f.properties.scalerank < 4),
      getSourcePosition: f => [-0.4531566, 51.4709959], // London
      getTargetPosition: f => f.geometry.coordinates,
      getSourceColor: [0, 128, 200],
      getTargetColor: [200, 0, 80],
      getWidth: 1
    })
  ]
});

export default function Map() {
  const { isLoaded } = useJsApiLoader({
    id: 'lensmap',
    googleMapsApiKey: "YOUR_API_KEY"
  })

  const onLoad = React.useCallback(function callback(map) {
    deckOverlay.setMap(map)
  }, [])

  const onUnmount = React.useCallback(function callback(map) {
  }, [])

  return isLoaded ? (
      <GoogleMap
        mapContainerStyle={getWindowDimensions()}
        center={center}
        zoom={10}
        onLoad={onLoad}
        onUnmount={onUnmount}
      >
        <></>
      </GoogleMap>
  ) : <></>
}

As you can see GoogleMapsOverlay receives a markers object in it's constructor, here I would get my markers doing a call to an API using axios but everything that I've tested ends in a 500 code when loading the page.

3
  • 1
    have you looked at the network request and see if any more specific error message is provided - it's pretty rare for a service like Google to just give a plain 500 with no info, especially if the request is due one of their public APIs. Commented Aug 4, 2022 at 20:37
  • We are missing some debugging information here. Seems like the error happen before you even use your variables. Commented Aug 4, 2022 at 20:48
  • If I'm understanding you correctly, the code you provided shows you passing a hard coded object to GoogleMapsOverlay as a reference to us to show what you are looking to get from the API. Your issue is that you are not successful in calling that API. If this is accurate, you can copy the request from the network tab of the development tools in your browser which could help understand why it fails. Commented Aug 4, 2022 at 22:39

2 Answers 2

1
+50

I assume that you're asking for a way to fetch the markers and make everything load in the correct order. I think you could store the deckOverlay instance in a ref, fetch the markers in a useEffect hook, update the layers with the markers data, and set a flag to hold from rendering the map until the layers are updated.

import React, { useState, useRef, useEffect, useCallback } from "react";
import { GoogleMap, useJsApiLoader } from "@react-google-maps/api";
import { GoogleMapsOverlay } from "@deck.gl/google-maps";
import { GeoJsonLayer, ArcLayer } from "@deck.gl/layers";
import axios from "axios";

const hasWindow = typeof window !== "undefined";

function getWindowDimensions() {
  const width = hasWindow ? window.innerWidth : null;
  const height = hasWindow ? window.innerHeight : null;
  return {
    width,
    height,
  };
}

const center = {
  lat: 51.509865,
  lng: -0.118092,
};

export default function Map() {
  const { isLoaded } = useJsApiLoader({
    id: "lensmap",
    googleMapsApiKey: "AIzaSyBmSBtlYQLH8jvAxrdgZErUdtdWLEs40gk",
  });
  const [markersLoaded, setMarkersLoaded] = useState(false);
  const deckOverlay = useRef(new GoogleMapsOverlay({ layers: [] }));
  const fecthMarkers = useCallback(async () => {
    try {
      const response = await axios.get(`someapi.com/markers`);
      // assuming API response will have a markers field
      const markers = response.data.markers;
      deckOverlay.current.setProps({
        layers: [
          new GeoJsonLayer({
            id: "airports",
            data: markers,
            filled: true,
            pointRadiusMinPixels: 2,
            opacity: 1,
            pointRadiusScale: 2000,
            getRadius: (f) => 11 - f.properties.scalerank,
            getFillColor: [200, 0, 80, 180],
            pickable: true,
            autoHighlight: true,
          }),
          new ArcLayer({
            id: "arcs",
            data: markers,
            dataTransform: (d) =>
              d.features.filter((f) => f.properties.scalerank < 4),
            getSourcePosition: (f) => [-0.4531566, 51.4709959], // London
            getTargetPosition: (f) => f.geometry.coordinates,
            getSourceColor: [0, 128, 200],
            getTargetColor: [200, 0, 80],
            getWidth: 1,
          }),
        ],
      });
      setMarkersLoaded(true);
    } catch (e) {
      // TODO: show some err UI
      console.log(e);
    }
  }, []);

  useEffect(() => {
    fecthMarkers();
  },[]);

  const onLoad = React.useCallback(function callback(map) {
    deckOverlay.current?.setMap(map);
  }, []);

  const onUnmount = React.useCallback(function callback(map) {
    deckOverlay.current?.finalize();
  }, []);

  return markersLoaded && isLoaded ? (
    <GoogleMap
      mapContainerStyle={getWindowDimensions()}
      center={center}
      zoom={10}
      onLoad={onLoad}
      onUnmount={onUnmount}
    >
      <></>
    </GoogleMap>
  ) : (
    <></>
  );
}
Sign up to request clarification or add additional context in comments.

1 Comment

The answer is mostly right, however using useCallback for fetchMarkers does not make sense as it is only called once in the effect (which runs once). You could instead pull fetchMarkers out of the component, make it return the markers, and move deckOverlay.current.setProps(...) and setMarkersLoaded(true) into the effect.
1

While it's a good idea to use a ref in most cases, it's not technically needed in this case, if there's just 1 instance of the component on the page. The important part is that you use an effect, which can run any JS and interact with any function / variable that is in scope.

Also important to know is that you need to add setMarkersLoaded(true); at the end to ensure a new render happens, if you want one to happen. If you don't need a render to happen (e.g. here if the map was already displayed regardless of whether the markers loaded), you can remove this part.

diedu's answer uses useCallback to create the async handler (fetchMarkers) used in useEffect, however you don't need to use this hook here. The function is written to ever be called just once, and is not passed to any component. useCallback is only for when you find a new function being created causes a component to re-render that otherwise wouldn't.

It's better to define the data fetching function outside of the component, so that you can keep the effect code simple and readable. You can even map it to layers in that function, and so remove another large chunk of logic out of your Map component.

useEffect(() => {
  (async () {
    const layers = await fetchMarkerLayers();
    deckOverlay.current.setProps({layers});
    setMarkersLoaded(true);
  })();
},[]);

Because the argument of useEffect can not be an async function, you need put a self invoking async function inside. If you don't like that syntax, you could also chain promises with .then. Both syntaxes are a bit hairy, but because we extracted the complex logic out of the component, it's still readable.

Full code

I kept some parts of diedu's snippet, like how the ref is used, as they didn't need changes.

import React, { useState, useRef, useEffect, useCallback } from "react";
import { GoogleMap, useJsApiLoader } from "@react-google-maps/api";
import { GoogleMapsOverlay } from "@deck.gl/google-maps";
import { GeoJsonLayer, ArcLayer } from "@deck.gl/layers";
import axios from "axios";

const hasWindow = typeof window !== "undefined";

function getWindowDimensions() {
  const width = hasWindow ? window.innerWidth : null;
  const height = hasWindow ? window.innerHeight : null;
  return {
    width,
    height,
  };
}

const center = {
  lat: 51.509865,
  lng: -0.118092,
};

const fetchMarkerLayers = async () => {
  try {
    const response = await axios.get(`someapi.com/markers`);
    // assuming API response will have a markers field
    const { markers } = response.data;
    return [
        new GeoJsonLayer({
          id: "airports",
          data: markers,
          filled: true,
          pointRadiusMinPixels: 2,
          opacity: 1,
          pointRadiusScale: 2000,
          getRadius: (f) => 11 - f.properties.scalerank,
          getFillColor: [200, 0, 80, 180],
          pickable: true,
          autoHighlight: true,
        }),
        new ArcLayer({
          id: "arcs",
          data: markers,
          dataTransform: (d) =>
            d.features.filter((f) => f.properties.scalerank < 4),
          getSourcePosition: (f) => [-0.4531566, 51.4709959], // London
          getTargetPosition: (f) => f.geometry.coordinates,
          getSourceColor: [0, 128, 200],
          getTargetColor: [200, 0, 80],
          getWidth: 1,
        }),
      ]
  } catch (e) {
    // TODO: show some err UI
    console.log(e);
  }
};


export default function Map() {
  const { isLoaded } = useJsApiLoader({
    id: "lensmap",
    googleMapsApiKey: "AIzaSyBmSBtlYQLH8jvAxrdgZErUdtdWLEs40gk",
  });
  const [markersLoaded, setMarkersLoaded] = useState(false);
  const deckOverlay = useRef(new GoogleMapsOverlay({ layers: [] }));
  
  useEffect(() => {
    // Use a self invoking async function because useEffect's argument function cannot be async.
    // Alternatively you can chain a regular Promise with `.then(layers => ...)`.
    (async () {
      const layers = await fetchMarkerLayers();
      deckOverlay.current.setProps({layers});
      setMarkersLoaded(true);
    })();
  },[]);

  const onLoad = React.useCallback(function callback(map) {
    deckOverlay.current?.setMap(map);
  }, []);

  const onUnmount = React.useCallback(function callback(map) {
    deckOverlay.current?.finalize();
  }, []);

  return markersLoaded && isLoaded ? (
    <GoogleMap
      mapContainerStyle={getWindowDimensions()}
      center={center}
      zoom={10}
      onLoad={onLoad}
      onUnmount={onUnmount}
    >
      <></>
    </GoogleMap>
  ) : (
    <></>
  );
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.