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.
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.
Mapcomponent renamed to
- Numerous prop changes both in
MapContainerand most other components
MapContainer‘s props are now immutable after mounting with the exception of
- No more
withLeafletHOC. It’s been replaced with
MapContainerhas dropped support for
New and Shiny 🚀
useMapEventhooks. 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
- Interactive documentation and examples
And these have been removed 🚮
- Classes, no more
createLeafletElement🎉. Everything is built on hooks and its better for it!
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 😆.
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 the
Map 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.
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
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
popupContainer like the old context did. I also took the opportunity to change
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:
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.
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
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
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
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
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
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
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.
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.