Problem: I'm building an interactive R Shiny app that uses leaflet to show a world map with country polygons and raster overlays (.tif) for each. Users can switch between the "World" view (aggregated polygons) and individual countries (raster data) by clicking on the leaflet. Issue:
When clicking on a country or switching back to "World", the map becomes blank/white. The same issue happens on the first load of the app: the map is white for a second before rendering the polygons and tiles.
After selecting a country map, a spinner appears:
then the map becomes completely white, without any visible raster and polygon:
The raster finally appears after a delay, showing correct data for the selected country:
What I already tried:
Shiny.setInputValue('raster_rendered', Math.random())
inside onRender(...)
to delay spinner removal.leafletProxy("map") %>% clearImages() %>% clearControls()
before every update.removeControl("legend_country")
instead of full clearControls()
.addTiles()
more than once — it's only in renderLeaflet()
.later::later(...)
and invalidateLater(...)
to delay map rendering.Code snippet from renderLeaflet()
:
output$map <- renderLeaflet({
leaflet(options = leafletOptions(minZoom = 1.8)) %>%
addProviderTiles("OpenStreetMap", group = "OpenStreetMap") %>%
addProviderTiles("Esri.WorldImagery", group = "Satellite") %>%
addLayersControl(
baseGroups = c("OpenStreetMap", "Satellite"),
position = "topright"
) %>%
onRender("
function(el, x) {
this.on('tileload', function() {
Shiny.setInputValue('raster_rendered', Math.random());
});
}
")
})
The one-second “white flash” (or a totally blank map while switching between World → Country → World) happens because the browser is still rendering the new polygon/raster layer while the old layer has already been cleared.
Nothing is “wrong” with the TIFFs or with leafletProxy()
—it is just a race condition between
the server telling Leaflet to add a new layer, and
the client repainting that layer and the base tiles.
output$map <- renderLeaflet({
leaflet(options = leafletOptions(minZoom = 1.8)) %>%
addProviderTiles("OpenStreetMap", group = "OpenStreetMap") %>%
addProviderTiles("Esri.WorldImagery", group = "Satellite") %>%
addLayersControl(
baseGroups = c("OpenStreetMap", "Satellite"),
position = "topright"
) %>%
onRender(
"
var map = this;
var fired = false;
// fires for every layer that is added later via leafletProxy()
map.on('layeradd', function(e) {
if (!fired && e.layer.options && e.layer.options.group === 'polygons') {
fired = true;
Shiny.setInputValue('raster_rendered', Math.random());
}
fired = false;
});
}
"
)
})
observeEvent(input$raster_rendered, {
remove_modal_spinner()
})
layeradd
is emitted for every geometry that Leaflet adds.
A local fired
flag guarantees that the callback body runs only for the very first geometry of the polygons group.
Because the listener remains attached (but guarded), you don’t get hundreds of messages, and you don’t need to re-register on every update.