Josep Bové
Josep Bové

Reputation: 688

Fetching data for another variable react

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.

Upvotes: 2

Views: 284

Answers (2)

inwerpsel
inwerpsel

Reputation: 3247

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

Upvotes: 1

diedu
diedu

Reputation: 20835

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

Upvotes: 1

Related Questions