Photo by T.H. Chia on Unsplash

Migrating react-leaflet from v2 to v3

A migration guide for react-leaflets latest major version (3). I’ll walk through some reasons to upgrade, whats changed, examples of what I had to change and even a special case involving two synced maps.

Sean Rennie
7 min readFeb 21, 2021

--

Paul LeCam’s react-leaflet library is a powerful set of React bindings for Leaflet. The latest major version overhauls the library to modern React, making use of hooks and doing away completely with classes. It’s also been re-written in Typescript 🎉! In my view this brings react-leaflet closer to the “wood” rather than a “React flavour” of Leaflet. The new core library makes building custom components really easy, giving React developers the full power of Leaflet and its plugin ecosystem.

Hats off to you Paul, thanks for the amazing work. Below is my attempt to add something to the community. Note: This article is by no means an exhaustive set of changes required to migrate.

So whats new in v3?

There are a some significant changes to the api.

Breaking 🚨

  • Map component renamed to MapContainer
  • Numerous prop changes both in MapContainer and most other components
  • MapContainer‘s props are now immutable after mounting with the exception of children
  • No more useLeaflet hook or withLeaflet HOC. It’s been replaced with useMap instead
  • MapContainer has dropped support for ref

New and Shiny 🚀

  • useMapEvents and useMapEvent hooks. They allow you to register map event listeners in any component and access its instance
  • Typescript, Typescript, Typescript. This is good news even if you don’t use TS. The intelli-sense and autocomplete is even better than before and is guaranteed to stay up to date
  • A core API ,@react-leaflet/core. You’ll only need this for building custom components. Hint, it’s particularly useful when you need a Leaflet plugin that doesn’t have a react-leaflet wrapper
  • Interactive documentation and examples

And these have been removed 🚮

  • Classes, no more createLeafletElement 🎉. Everything is built on hooks and its better for it!
  • withLeaflet and useLeaflet

Everyone should think about updating

Everyone should consider upgrading to v3 not only because v2 is no longer maintained but because (IMHO) v3 is much closer to using Leaflet than v2 was. I found myself reaching for the Leaflet docs way more. I also feel like it’s helped me get a much better grasp on Leaflet internals and how to manipulate it in React (the secret, write Leaflet code). Because Leaflet controls the DOM approaching things with a React first approach lead me astray. Ultimately every react-leaflet component renders null in React land.

If you use Leaflet to render a simple map with a tile layer or two then your changes will be minimal. If you have a host of custom components or any special cases (like syncing two maps) then the migration will be more involved. If you’ve been wanting to use Leaflet but can’t because your case requires a Leaflet plugin that doesn’t have a react-wrapper, now is the perfect time to dive in. Building custom components is a real treat in v3. That said, I won’t cover custom components for plugins in this post, but I hope too in the near future.

You get it done by following the errors

Below I’ll illustrate the whats needed to upgrade to v3. Following the errors in the console as well as the type errors from the compiler highlighted the way. Roughly speaking I did things in the order written but I didn’t keep any devlogs while migrating so there are probably some mistakes. Follow along loosely for best results 😆.

The basics

Upgrade react-leaflet with your favourite package manager. If you use typescript, remove @types/react-leaflet. V3 has them built in so this ensures no compiler issues.

Giving over control to MapContainer

V3 removes theMap component replacing it with MapContainer and is one of the biggest changes. In v2 we might have done this:

This will look really familiar as its pretty much a controlled component. V2 allowed us to use this common React pattern to govern the state of our map but at the end of the day its Leaflet that controls the DOM so letting it control its own internal state makes more sense. Also, the number of if statements in the old v2 Map component was obscene and I’m sure Paul wanted to simplify things. Why write again in React what has already been taken care of in Leaflet!

V3 gets rid of the controllable Map component and replaces it with MapContainer. The key difference here is that the MapContainer props are immutable its mounted(except children). You can set the start position of your map as well as any options you need but you cannot update them and expect the map to react. We are now forced to use Leaflet methods on the map instance to interact with it however you need. Here’s what the above example looks like after its migrated:

Another key difference is that MapContainer no longer has a ref attribute. Paul indicates it’s been problematic so he’s removed it as a mechanism to get an instance of the map. Don’t worry though, there are a few options to get at the map instance. You could use any of the new hooks, or if you need access to it outside of MapContainer grab a reference using the whenCreated prop. These issues on Github here and here have examples doing just that.

Hooks galore

In v2 you could access a map instance and various other pieces of context with the useLeaflet hook or via a prop with the withLeaflet HOC. In v3 both of these have been removed and replaced with useMap, useMapEvent and useMapEvents. which all return a map instance.

I found searching the codebase for useLeaflet was the most efficient way to update it. Don’t do a find a replace though. The useMap hook only returns an instance of the map, it doesn’t include the other properties pane, layerContainer & popupContainer like the old context did. I also took the opportunity to change map to mapInstance, making it clearer what the reference is.

If you needed any of the other properties, get them by calling the appropriate method on the map instance:

The useMapEvent and useMapEvents hooks are for attaching Leaflet event listeners to any child component of MapContainer. Be sure to check out the docs for some examples or there’s one of my own coming below.

Prop Changes

Many of the components will have some prop changes because in v3 they reflect the underlying Leaflet class options more closely. I leaned heavily on the tsc compiler, running it in watch mode tsc --watch and working my way through the errors. Our app exclusively uses GeoJSON and TileLayer and the latter didn’t need any changes. Each instance of GeoJSON needed the event handlers moved to the eventHandler prop and the renderer moved into pathOptions like this:

That was pretty much it for changes needed to update the react-leaflet api.

Syncing two maps. A special case

The final and biggest challenge was to migrate our map comparison mode. I’d disabled it while dealing with the main api updates.

The comparison mode feature needs to show two, almost identical, maps side by side. These maps are synced, if you pan or zoom one, the other will pan or zoom too. To do this you need access to the map instance of each map, only trigger a sync on a user interaction. In v2 we achieved this using leaflet.sync. The library served us well but its been unmaintained for 3 years and it gave me headaches during the upgrade. Eventually I settled on building a custom MapSync component to handle the behaviour we wanted. I found this to work better and was easier to reason about too.

Here’s what it looks like:

The component is rendered by each map when in comparison mode. MapSync listens for drag and zoom events using useMapEvents when a users interacts with it. The hook returns the map instance of the interacted map and we pass in an instance of the otherMap as a prop, conveniently saved by the parent wrapper. Event listeners then trigger a sync of the otherMap (map1) where the sync() function sets the centre & zoom of it to that of the interacted one (map2) using map instance methods. A useEffect allows for syncing the maps when the component mounts, starting both maps off with the same position.

MapSync is used like this:

Let’s walk through whats happening. The Maps component is passed either one or two sets of map layers depending on the isComparing flag. React.toArray(children) turns them into an array to be mapped out creating the required number of MapContainers. Here, using the array index, there is all the control needed to save refs, set styles and prime the maps with an initial zoom and centre. The div that wraps MapContainer is for dynamically change the map widths because it’s not possible to change the classNames prop on the first map when we add the second one. Remember, all props for MapContainer are immutable except for children.

Using whenCreated the refs of each map instance are saved for later. The helper functions ensure that the correct centre and zoom are used when the second map mounts, they also ensure that the page does not crash if a user refreshes. It’s easy to control what the second maps position should be when the user enters compare mode, it’s what ever the zoom and centre of the first map are. But if a user refreshes the page while in compare mode? The second map would expect the first maps ref to exist (which it won’t yet) and cause a crash. If a page has refreshed we can safely assume both maps should use the INITIAL_* values.

In comparison mode we can render MapSync in each map passing it the instance of the other map which we saved when MapContainer mounted. While mounted MapSync takes care of positioning the opposing map when a user interacts with either one.

I’m pretty happy with this outcome, I had some help from a stack overflow but sadly I can’t find the page to link here. The useMapEvents hook and the whenCreated prop really helped me out too. Dropping an unmaintained library keeps the maintenance overhead in our app down.

Conclusion

I’ve loved migrating to react-leaflet v3. I think its an amazing update from the previous version and gives me a lot of control over Leaflet in my React project.

I’d love to hear any comments (good or bad) in the comments below.

Happy Coding.

--

--

Sean Rennie

Software engineer with a love of maps and anything geospatial. I enjoy techno, the outdoors, tequila & coffee. Currently working at Sensat Ltd