0

I am building an app with React, TS, Mapbox, react-mapbox-gl and RecoilJS.

This is the Dashboard component in which I'm working:

import React from "react";
import PageTemplate from "../../components/PageTemplate/PageTemplate";

import ReactMapboxGl from "react-mapbox-gl";

import { CGOIncidentsMapConfig } from "./Map/CGOIncidents";
import { zoomSuscriberState, clickSuscriberState } from "../../contexts/MapContext";
import { useRecoilValue } from "recoil";
import { MapEvent } from "react-mapbox-gl/lib/map-events";

const Map = ReactMapboxGl({
  accessToken: process.env.REACT_APP_MAPBOX_TOKEN ?? "",
});

const getEventHandle: (subscribers: MapEvent[]) => MapEvent = (subscribers) => (map, evt) => {
  console.log(subscribers);
  for (const event of subscribers) {
    event(map, evt);
  }
};

const Dashboard: React.FC = () => {
  const zoomSuscribers = useRecoilValue(zoomSuscriberState);
  const clickSuscribers = useRecoilValue(clickSuscriberState);

  console.log(zoomSuscribers);
  console.log(clickSuscribers);

  return (
    <PageTemplate>
      <Map
        style="mapbox://styles/mapbox/light-v10"
        containerStyle={{
          height: "100%",
          width: "100%",
        }}
        center={[-3.7, 40.44]}
        zoom={[5]}
        onZoom={getEventHandle(zoomSuscribers)}
        onClick={getEventHandle(clickSuscribers)}
      >
        <CGOIncidentsMapConfig />
      </Map>
    </PageTemplate>
  );
};

export default Dashboard;

This is the CGOIncidentsMapConfig component simplified:


export const CGOIncidentsMapConfig: React.FC = () => {
  const [popupCoordinates, setPopupCoordinates] = useState<number[]>([-0.13235092163085938, 51.518250335096376]);
  const [isPopupVisible, setPopupVisible] = useState<boolean>(false);
  const [popopContainer, setPopupContainer] = useState("");
  const [leyend, setLeyend] = useState<{
    title: string;
    elements: {
      title: string;
      color: string;
    }[];
  }>(LEYENDS.PROVINCES);

  const onZoomMapEvent: MapEvent = (map) => {
    console.log("ZIIIM", map.getZoom());
  };

  const onMouseEnterEvent = (e: MapLayerMouseEvent) => {
    const map = e.target;
    map.getCanvas().style.cursor = "pointer";
  };

  const onMouseLeaveEvent = (e: MapLayerMouseEvent) => {
    const map = e.target;
    map.getCanvas().style.cursor = "";
  };

  const dismissPopup = () => {
    setPopupVisible(false);
    console.log("CIIM");
  };

  const setZoomSuscribers = useSetRecoilState(zoomSuscriberState);
  const setClickSuscribers = useSetRecoilState(clickSuscriberState);

  useEffect(() => {
    setZoomSuscribers((oldZoomSubscribers) => [...oldZoomSubscribers, onZoomMapEvent]);

    setClickSuscribers((oldClickSubscribers) => [...oldClickSubscribers, dismissPopup]);

    console.log("updated recoil");
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  console.log(isPopupVisible);

  return (
    <Fragment>
      <Source id={CGO_INCIDENTS_SOURCE_ID} tileJsonSource={CGO_INCIDENTS_SOURCE_OPTIONS} />
      <Layer
        id={CGO_INCIDENTS_LAYER_LAYOUT.id}
        type={CGO_INCIDENTS_LAYER_LAYOUT.type}
        sourceId={CGO_INCIDENTS_LAYER_LAYOUT.source}
        sourceLayer={CGO_INCIDENTS_LAYER_LAYOUT["source-layer"]}
        paint={CGO_INCIDENTS_LAYER_LAYOUT.paint}
        onClick={onClickEvent}
        onMouseEnter={onMouseEnterEvent}
        onMouseLeave={onMouseLeaveEvent}
      />
      {isPopupVisible && (
        <Popup coordinates={popupCoordinates} onClick={dismissPopup}>
          {popopContainer}
        </Popup>
      )}
      <Leyend title={leyend.title} elements={leyend.elements} />
    </Fragment>
  );
};

This is my MapContext file in which I created the atoms:

import { MapEvent } from "react-mapbox-gl/lib/map-events";
import { atom } from "recoil";

export const zoomSuscriberState = atom<MapEvent[]>({ key: "zoomSuscriberState", default: [] });
export const clickSuscriberState = atom<MapEvent[]>({ key: "clickSuscriberState", default: [] });

As you can see, what I want to do is just to create a subscriber pattern for the onClick and onZoom events with RecoilJS. My idea with this is to separate each Mapbox layer in different components.

When the function returned by getEventHandler is executed, the console.log inside only displays an empty array for zoomSuscribers or clickSuscribers but what I see with the console.log into the Dashboard component is that both atoms have a callback function settled by the CGOIncidentsMapConfig component. These atoms are empty at the first render of Dashboard and fulfilled once CGOIncidentsMapConfig component has been rendered. Seems like RecoilJS updated both atoms and the Dashboard component, as expected.

Why the console.log inside the getEventHandler functions only prints an empty array? Therefore, the event are not properly executed by the subscribers.

Thanks in advance.

2 Answers2

0

That is because you are calling the getEventHandle function instead of passing it to the Map component.

Change these lines:

onZoom={getEventHandle(zoomSuscribers)}
onClick={getEventHandle(clickSuscribers)}

to

onZoom={() => getEventHandle(zoomSuscribers)}
onClick={() => getEventHandle(clickSuscribers)}
Johannes Klauß
  • 10,676
  • 16
  • 68
  • 122
  • First, thanks for your response. Sadly it doesn't make sense: `onZoom` and `onClick` are properties that need a function with two parameters: map and event. The change that you propouse should be something like this: ``` onZoom={(map,evt) => getEventHandle(zoomSuscribers)(map,evt)} onClick={(map,evit) => getEventHandle(clickSuscribers)(map,evt)} ``` Sadly this won't fix my problem because it's the same. Thanks anyway for your time :) – Cristian Fernandez May 19 '21 at 10:58
0

I solved it!

The solution whas with useRecoilCallback:

import React from "react";
import PageTemplate from "../../components/PageTemplate/PageTemplate";

import ReactMapboxGl from "react-mapbox-gl";

import { CGOIncidentsMapConfig } from "./Map/CGOIncidents";
import { zoomSuscriberState, clickSuscriberState } from "../../contexts/MapContext";
import { useRecoilCallback, useRecoilValue } from "recoil";
import { MapEvent } from "react-mapbox-gl/lib/map-events";

const Map = ReactMapboxGl({
  accessToken: process.env.REACT_APP_MAPBOX_TOKEN ?? "",
});

const Dashboard: React.FC = () => {
  const onZoom: MapEvent = useRecoilCallback(({ snapshot }) => async (map, evt) => {
    const subs = await snapshot.getPromise(zoomSuscriberState);
    for (const event of subs) {
      event(map, evt);
    }
  });

  const onClick: MapEvent = useRecoilCallback(({ snapshot }) => async (map, evt) => {
    const subs = await snapshot.getPromise(clickSuscriberState);
    for (const event of subs) {
      event(map, evt);
    }
  });

  return (
    <PageTemplate>
      <Map
        style="mapbox://styles/mapbox/light-v10"
        containerStyle={{
          height: "100%",
          width: "100%",
        }}
        center={[-3.7, 40.44]}
        zoom={[5]}
        onZoom={onZoom}
        onClick={onClick}
      >
        <CGOIncidentsMapConfig />
      </Map>
    </PageTemplate>
  );
};

export default Dashboard;