Exploring a React renderer for GeoJSON maps
Exploring a React renderer for GeoJSON maps
Finding a project for learning React Reconciler
I used OpenLayers frequently for map visualizations. While looking for a small project to study React Reconciler, I considered building something like React Three Fiber for OpenLayers.
I looked for declarative OpenLayers packages and found that Planet Labs already had a project in that space. I decided to narrow my experiment to GeoJSON visualization. That became react-geojson-map.
The project also gave me a place to explore how to organize styling code. I wanted to define colors in CSS and use those values when rendering the map on Canvas.
A ten-line goal for GeoJSON rendering
I set a goal for the package: getting GeoJSON onto a map should take no more than ten lines of application code. This was the first project where I thought explicitly about usability while designing the API.
Connecting a visualization library to React’s lifecycle made me concerned about the overhead the integration could introduce. I also had to consider how much freedom to expose for more flexible uses. The ten-line goal kept my attention on making the intended GeoJSON use case easy to start with.
I supplied defaults and kept the API narrow. For styling, I exposed a predefined set of CSS custom properties. Users could change their values, but the package determined which settings were available. This kept the starting example short.
Rendering GeoJSON through React
The experiment uses a custom React renderer to translate JSX declarations into OpenLayers objects. With the components imported and the map’s parent given a size, a data source can be declared like this:
<GeoMap className="map-theme">
<GeoDataSource fitViewToData url="/data/sample.geojson" />
</GeoMap>
GeoMap creates an OpenLayers map and supplies it as the container for a separate React root. The host implementation creates a GeoJSON source and vector layer, attaches the layer to the map, handles URL updates, and removes the layer when its element is removed. React calls those host methods during reconciliation.
The prototype includes fitViewToData for fitting the view to the data with padding and a maximum zoom. It also exposes onClick, onHover, and onMissed callbacks. The data-source renderer registers OpenLayers event listeners and passes feature properties to the click and hover callbacks when a feature is found.
Treating colors as styling
I was a junior developer at the time, thinking about where different kinds of code belonged. Even when Canvas rendering code became tangled, color settings still felt like styling. I wanted to try keeping those settings in CSS.
The implementation reads vector-layer style settings from CSS custom properties. For example, these properties control polygon fill and stroke:
.map-theme {
--data-source-polygon-fill-color: rgba(126, 188, 111, 0.1);
--data-source-polygon-stroke-color: rgba(91, 124, 186, 1);
--data-source-polygon-stroke-width: 2;
}
The style mapping defines the supported properties and fallback values. A MutationObserver watches the map container’s class and inline style attributes. When those attributes change, the renderer reads the computed CSS values and reapplies the layer style without requiring a React re-render.
I also made CSS changes work with hot module replacement (HMR) in my development setup. Editing CSS variables in Chrome DevTools exposed a limitation: the change-detection mechanism I had implemented did not immediately reflect those edits in the Canvas drawing. The observer watches attributes on the map container; it does not observe every change to the stylesheets or inherited values that determine a computed CSS property.
Scope of the experiment
The source snapshot linked above does not provide complete cleanup. Layer removal does not by itself release the event listeners or disconnect the style observer, and GeoMap does not yet unmount the custom React root or dispose of its OpenLayers map on unmount.
Event handling also needs more work for multiple layers. The data-source renderer queries features at the pointer’s pixel without filtering them to that data source’s layer. These are limits of the prototype’s integration with OpenLayers.
What I found satisfying
Moving the color settings into CSS helped me organize the code. I was satisfied with separating the responsibilities: CSS for styling, JSX as markup for declaring data sources and map composition, and JavaScript for rendering. That separation mattered to me even though live editing still had limitations.