DEV Community

Cover image for Creating map components in  ReasonReact with bindings for LeafletJS
Ashutosh Sharma
Ashutosh Sharma

Posted on

Creating map components in ReasonReact with bindings for LeafletJS

The ReasonML community is expanding everyday with a growing number of users and many more are getting inspired to adopt. Most of these adopters have been previously writing code in Javascript and there are some compelling reasons for their migration to ReasonML.

A few years ago, with the advent of React, JS developers were opening up to concepts of functional programming and its benefits for concurrency. While Typescript was still coming of age with its roots in JS, a strongly typed language was needed for web development which is aligned with fundamentals of functional programming. ReasonML was built right for this purpose, providing the best of JS and OCaml environments. Through OCaml’s FFI, ReScript provided excellent bridges between these environments which enabled developers to build some fine projects using ReasonReact. This was a key factor behind the popularity of ReasonReact.

The JS environment is rich with many useful libraries, which ReasonML developers frequently feel the need to use. As a result writing efficient bindings becomes crucial. Bindings are APIs that allow developers to use libraries from other environments. OCaml’s FFI allows developers to use JS libraries through these bindings (ReScript docs). Even though ReScript keeps on adding libraries to their offering, a myriad of libraries are still unavailable.

This article will demonstrate utilization of one such library called LeafletJS, for which bindings will be written and used in accordance with React philosophy. The aim here is to display a map and place a marker on it through React components using functions from LeafletJS. The source code for this exercise is available in bs-leaflet-cmp, which can also be used for initial setup.

Having identified the desired library, next step is to include it in the project as recommended here, by adding following code to index.html

  <link rel="stylesheet"
href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"
integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
crossorigin="" />
  <script
src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"
integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
crossorigin=""></script>
Enter fullscreen mode Exit fullscreen mode

First goal is to create a component which renders a map. To attain this, first step is to write some bindings for the required functions from LeafletJS. Here is a sample binding, which can also be found in MapBinding.re:

[@bs.scope "L"] [@bs.new]
external createMap: (string, Map.options) => Map.t = "map";
Enter fullscreen mode Exit fullscreen mode

This accepts a string, which is the DOM ID of a div, and optionally an object literal for options. It instantiates and returns a map object of type Map.t.

Writing bindings also involves declaring the ReasonML types compatible with target APIs, as shown above. To facilitate this, ReasonML has some shared bidirectional data types, thus omitting the need for data-converters.

Next step is to create a React component MapCmp with render props which enables all subsequent map-elements to use the map object. This component can be found at MapCmp.re. It is responsible for instantiating map object for the rendered map-div followed by creating and adding a tilelayer to the map and rendering children. The useEffect0 for this component is written in following manner to accomplish this:

React.useEffect0(() => {
    let mapObj = MapBinding.createMap(
                   mapOptions.map_elem_id,
                   mapOpts
                 );
    let tileLayer =
      MapBinding.createTileLayer(
        "https://tile.thunderforest.com/neighbourhood/
        {z}/{x}/{y}.png?apikey=<ApiKey>",
        {
          attribution: "Maps &copy;
            <a href='https://www.thunderforest.com/'>
              Thunderforest
            </a>
            , Data &copy;
            <a href='http://www.openstreetmap.org/copyright'>
              OpenStreetMap contributors
            </a>",
          minZoom: 11,
          maxZoom: 15,
        },
      );
    MapBinding.addLayerToMap(tileLayer, mapObj)->ignore;
    None;
  });
Enter fullscreen mode Exit fullscreen mode

After creating a component to render a map, next goal is to create a component for placing marker. First step towards that is writing a binding for a function to create marker. This binding can be found in MarkerBinding.re:

external createMarker:
  (~pos: LatLng.t,
   ~opts: option(Marker.options)=?,
   unit) => Marker.t = "marker";
Enter fullscreen mode Exit fullscreen mode

Next step would be writing a component which creates and adds a marker on the given map. To achieve this, bindings are used in following manner in the useEffect0 for this component:

React.useEffect0(() => {
    let marker =
      MarkerBinding.createMarker(
        ~pos=marker_props.location,
        ~opts=marker_props.marker_options,
        (),
      );
    MarkerBinding.addMarkerToMap(marker, map) |> ignore;
    dispatch(SetMyMarker(Some(marker)));
    Some(() => MarkerBinding.removeMarkerFromMap(
                 marker, map
               )->ignore
    );
  });
Enter fullscreen mode Exit fullscreen mode

Important to note that it removes marker from the map upon cleanup. There are other features and effects which can be added to the marker component. For example, if the marker position is updated in props, then a useEffect can be added to update marker location in accordance:

  React.useEffect1(
    () => {
      switch (state.marker) {
      | Some(marker) =>
        MarkerBinding.setMarkerLatLng(
          marker, marker_props.location
        ) |> ignore
      | _ => ()
      };
      None;
    },
    [|marker_props.location|],
  );
Enter fullscreen mode Exit fullscreen mode

The usage of MapCmp and MarkerCmp is shown in ExampleComponent.re in the repo. Note that the repo contains more such components and features. These components are used as shown below

<MapCmp mapOptions={
          map_elem_id: "map_div",
          options: {
            center: {lat: 13.0, lng: 77.60},
            zoom: 12,
          }}>
    {map => <MarkerCmp
               map
               marker_props={
                 location: { lat: 13.0,lng: 77.60},
                 marker_options: None}>
           </MarkerCmp>}
</MapCmp>
Enter fullscreen mode Exit fullscreen mode

The generated result looks like this:

leafletjs map with thunderforest

The aim of this article was to share an approach for using ReasonML-JS bindings through react components. This makes code very scalable and structured, which was demonstrated with the help of a use-case involving maps. Though the bindings and components presented here are most basic in nature and were intended to highlight this concept, with careful design they have a potential of scaling to smoothly handle complex use-cases.

Top comments (0)