<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Frontend on Samitha Widanage</title>
    <link>https://samithahansaka.com/tags/frontend/</link>
    <description>Recent content in Frontend on Samitha Widanage</description>
    <generator>Hugo -- 0.162.1</generator>
    <language>en-us</language>
    <lastBuildDate>Wed, 15 Apr 2020 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://samithahansaka.com/tags/frontend/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Clustering Many Markers in google-map-react</title>
      <link>https://samithahansaka.com/posts/clustering-google-map-react/</link>
      <pubDate>Wed, 15 Apr 2020 00:00:00 +0000</pubDate>
      <guid>https://samithahansaka.com/posts/clustering-google-map-react/</guid>
      <description>How to render thousands of markers on a Google Map with React, without melting the browser. Real bugs, real fixes.</description>
      <content:encoded><![CDATA[<p>You drop 5,000 markers onto a Google Map in React. The browser fan kicks in. Pan lag. CPU pegged. Users complain.</p>
<p>I&rsquo;ve been there. The fix is clustering: aggregate nearby markers into a single visual until the user zooms in. Google&rsquo;s Marker Clustering API does this natively, but it only renders raw image markers. I needed <em>actual</em> React components as my markers and clusters. Material-UI popovers, animations, Redux-aware state, custom interactions. That meant rolling my own stack.</p>
<p>This is the pattern I landed on after a few rebuilds. Plus five gotchas that ate weeks of my time. Writing it down so you don&rsquo;t have to repeat them.</p>
<blockquote>
<p><strong>Heads up before you read.</strong> I shipped this around 2020, so the stack is roughly 5 years old at this point. The pattern still holds up well, but the React map ecosystem has moved on. The three-component split, the gotchas, and the fixes are all still relevant. The specific libraries are not what I&rsquo;d reach for today. I&rsquo;ve added a <em>&ldquo;What I&rsquo;d use in 2026&rdquo;</em> section at the end so you can see both sides.</p>
</blockquote>
<h2 id="the-stack">The stack</h2>
<ul>
<li><a href="https://github.com/google-map-react/google-map-react">google-map-react</a> renders React components on top of a Google Map at real lat/lng coordinates.</li>
<li><a href="https://github.com/anatoo/points-cluster">points-cluster</a> is a pure-JS clustering algorithm. Give it points, bounds, and zoom. Get back clusters.</li>
<li><a href="https://mui.com">Material-UI</a> for the info window popover.</li>
<li><a href="https://redux.js.org">Redux</a> and <a href="https://react-redux.js.org">react-redux</a> for selected-marker state.</li>
</ul>
<p>The library does have clustering examples, but they&rsquo;re written in older React idioms (class components, lifecycle methods). Here&rsquo;s a modern hook-based version that mirrors how I shipped it.</p>
<h2 id="the-three-component-split">The three-component split</h2>
<p>At the core, clustering breaks into three jobs:</p>
<ol>
<li><code>GoogleMap</code> owns the map instance, points data, current bounds and zoom, and runs the clustering algorithm.</li>
<li><code>Cluster</code> renders an aggregated marker when several points sit close together.</li>
<li><code>Marker</code> renders a single point when zoomed in or isolated.</li>
</ol>
<h3 id="1-googlemapjs">1. <code>GoogleMap.js</code></h3>
<p>The map owns everything. Bounds, zoom, the points list, and the clustering call. Here&rsquo;s what mine looks like:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-jsx" data-lang="jsx"><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">React</span><span class="p">,</span> <span class="p">{</span> <span class="nx">useState</span><span class="p">,</span> <span class="nx">useMemo</span> <span class="p">}</span> <span class="nx">from</span> <span class="s2">&#34;react&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">GoogleMapReact</span> <span class="nx">from</span> <span class="s2">&#34;google-map-react&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">supercluster</span> <span class="nx">from</span> <span class="s2">&#34;points-cluster&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">Marker</span> <span class="nx">from</span> <span class="s2">&#34;./Marker&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">Cluster</span> <span class="nx">from</span> <span class="s2">&#34;./Cluster&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">GoogleMap</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">points</span><span class="p">,</span> <span class="nx">apiKey</span><span class="p">,</span> <span class="nx">defaultCenter</span><span class="p">,</span> <span class="nx">defaultZoom</span> <span class="p">})</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="p">[</span><span class="nx">mapProps</span><span class="p">,</span> <span class="nx">setMapProps</span><span class="p">]</span> <span class="o">=</span> <span class="nx">useState</span><span class="p">({</span>
</span></span><span class="line"><span class="cl">    <span class="nx">center</span><span class="o">:</span> <span class="nx">defaultCenter</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">zoom</span><span class="o">:</span> <span class="nx">defaultZoom</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="nx">bounds</span><span class="o">:</span> <span class="kc">null</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">});</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">clusters</span> <span class="o">=</span> <span class="nx">useMemo</span><span class="p">(()</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">mapProps</span><span class="p">.</span><span class="nx">bounds</span><span class="p">)</span> <span class="k">return</span> <span class="p">[];</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nx">supercluster</span><span class="p">(</span><span class="nx">points</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">minZoom</span><span class="o">:</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">maxZoom</span><span class="o">:</span> <span class="mi">16</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nx">radius</span><span class="o">:</span> <span class="mi">60</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">})({</span> <span class="nx">bounds</span><span class="o">:</span> <span class="nx">mapProps</span><span class="p">.</span><span class="nx">bounds</span><span class="p">,</span> <span class="nx">zoom</span><span class="o">:</span> <span class="nx">mapProps</span><span class="p">.</span><span class="nx">zoom</span> <span class="p">});</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span> <span class="p">[</span><span class="nx">points</span><span class="p">,</span> <span class="nx">mapProps</span><span class="p">.</span><span class="nx">bounds</span><span class="p">,</span> <span class="nx">mapProps</span><span class="p">.</span><span class="nx">zoom</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;</span><span class="nt">div</span> <span class="na">style</span><span class="o">=</span><span class="p">{{</span> <span class="nx">height</span><span class="o">:</span> <span class="s2">&#34;100vh&#34;</span><span class="p">,</span> <span class="nx">width</span><span class="o">:</span> <span class="s2">&#34;100%&#34;</span> <span class="p">}}&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">&lt;</span><span class="nt">GoogleMapReact</span>
</span></span><span class="line"><span class="cl">        <span class="na">bootstrapURLKeys</span><span class="o">=</span><span class="p">{{</span> <span class="nx">key</span><span class="o">:</span> <span class="nx">apiKey</span> <span class="p">}}</span>
</span></span><span class="line"><span class="cl">        <span class="na">defaultCenter</span><span class="o">=</span><span class="p">{</span><span class="nx">defaultCenter</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="na">defaultZoom</span><span class="o">=</span><span class="p">{</span><span class="nx">defaultZoom</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="na">yesIWantToUseGoogleMapApiInternals</span>
</span></span><span class="line"><span class="cl">        <span class="na">onChange</span><span class="o">=</span><span class="p">{({</span> <span class="nx">center</span><span class="p">,</span> <span class="nx">zoom</span><span class="p">,</span> <span class="nx">bounds</span> <span class="p">})</span> <span class="p">=&gt;</span>
</span></span><span class="line"><span class="cl">          <span class="nx">setMapProps</span><span class="p">({</span> <span class="nx">center</span><span class="p">,</span> <span class="nx">zoom</span><span class="p">,</span> <span class="nx">bounds</span> <span class="p">})</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="na">options</span><span class="o">=</span><span class="p">{{</span>
</span></span><span class="line"><span class="cl">          <span class="nx">restriction</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="nx">latLngBounds</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">              <span class="nx">north</span><span class="o">:</span> <span class="mi">85</span><span class="p">,</span> <span class="nx">south</span><span class="o">:</span> <span class="o">-</span><span class="mi">85</span><span class="p">,</span> <span class="nx">west</span><span class="o">:</span> <span class="o">-</span><span class="mi">180</span><span class="p">,</span> <span class="nx">east</span><span class="o">:</span> <span class="mi">180</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">            <span class="p">},</span>
</span></span><span class="line"><span class="cl">            <span class="nx">strictBounds</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">          <span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="p">}}</span>
</span></span><span class="line"><span class="cl">      <span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span><span class="nx">clusters</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">c</span><span class="p">)</span> <span class="p">=&gt;</span>
</span></span><span class="line"><span class="cl">          <span class="nx">c</span><span class="p">.</span><span class="nx">numPoints</span> <span class="o">===</span> <span class="mi">1</span> <span class="o">?</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="p">&lt;</span><span class="nt">Marker</span>
</span></span><span class="line"><span class="cl">              <span class="na">key</span><span class="o">=</span><span class="p">{</span><span class="nx">c</span><span class="p">.</span><span class="nx">points</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">id</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">              <span class="na">lat</span><span class="o">=</span><span class="p">{</span><span class="nx">c</span><span class="p">.</span><span class="nx">y</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">              <span class="na">lng</span><span class="o">=</span><span class="p">{</span><span class="nx">c</span><span class="p">.</span><span class="nx">x</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">              <span class="na">point</span><span class="o">=</span><span class="p">{</span><span class="nx">c</span><span class="p">.</span><span class="nx">points</span><span class="p">[</span><span class="mi">0</span><span class="p">]}</span>
</span></span><span class="line"><span class="cl">            <span class="p">/&gt;</span>
</span></span><span class="line"><span class="cl">          <span class="p">)</span> <span class="o">:</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">            <span class="p">&lt;</span><span class="nt">Cluster</span>
</span></span><span class="line"><span class="cl">              <span class="na">key</span><span class="o">=</span><span class="p">{</span><span class="sb">`</span><span class="si">${</span><span class="nx">c</span><span class="p">.</span><span class="nx">x</span><span class="si">}</span><span class="sb">-</span><span class="si">${</span><span class="nx">c</span><span class="p">.</span><span class="nx">y</span><span class="si">}</span><span class="sb">-</span><span class="si">${</span><span class="nx">c</span><span class="p">.</span><span class="nx">numPoints</span><span class="si">}</span><span class="sb">`</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">              <span class="na">lat</span><span class="o">=</span><span class="p">{</span><span class="nx">c</span><span class="p">.</span><span class="nx">y</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">              <span class="na">lng</span><span class="o">=</span><span class="p">{</span><span class="nx">c</span><span class="p">.</span><span class="nx">x</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">              <span class="na">count</span><span class="o">=</span><span class="p">{</span><span class="nx">c</span><span class="p">.</span><span class="nx">numPoints</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="p">/&gt;</span>
</span></span><span class="line"><span class="cl">          <span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="p">)}</span>
</span></span><span class="line"><span class="cl">      <span class="p">&lt;/</span><span class="nt">GoogleMapReact</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="k">default</span> <span class="nx">GoogleMap</span><span class="p">;</span>
</span></span></code></pre></div><h3 id="2-clusterjs">2. <code>Cluster.js</code></h3>
<p>A circle with a number on it, sized by density. That&rsquo;s it.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-jsx" data-lang="jsx"><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">React</span> <span class="nx">from</span> <span class="s2">&#34;react&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">styled</span> <span class="nx">from</span> <span class="s2">&#34;styled-components&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">ClusterBubble</span> <span class="o">=</span> <span class="nx">styled</span><span class="p">.</span><span class="nx">div</span><span class="sb">`
</span></span></span><span class="line"><span class="cl"><span class="sb">  width: </span><span class="si">${</span><span class="p">(</span><span class="nx">p</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="nx">p</span><span class="p">.</span><span class="nx">size</span><span class="si">}</span><span class="sb">px;
</span></span></span><span class="line"><span class="cl"><span class="sb">  height: </span><span class="si">${</span><span class="p">(</span><span class="nx">p</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="nx">p</span><span class="p">.</span><span class="nx">size</span><span class="si">}</span><span class="sb">px;
</span></span></span><span class="line"><span class="cl"><span class="sb">  border-radius: 50%;
</span></span></span><span class="line"><span class="cl"><span class="sb">  background: </span><span class="si">${</span><span class="p">(</span><span class="nx">p</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="nx">p</span><span class="p">.</span><span class="nx">color</span><span class="si">}</span><span class="sb">;
</span></span></span><span class="line"><span class="cl"><span class="sb">  color: white;
</span></span></span><span class="line"><span class="cl"><span class="sb">  display: flex;
</span></span></span><span class="line"><span class="cl"><span class="sb">  align-items: center;
</span></span></span><span class="line"><span class="cl"><span class="sb">  justify-content: center;
</span></span></span><span class="line"><span class="cl"><span class="sb">  font-weight: 600;
</span></span></span><span class="line"><span class="cl"><span class="sb">  cursor: pointer;
</span></span></span><span class="line"><span class="cl"><span class="sb">  transform: translate(-50%, -50%);
</span></span></span><span class="line"><span class="cl"><span class="sb">`</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">getStyle</span> <span class="o">=</span> <span class="p">(</span><span class="nx">count</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">count</span> <span class="o">&lt;</span> <span class="mi">10</span><span class="p">)</span>   <span class="k">return</span> <span class="p">{</span> <span class="nx">size</span><span class="o">:</span> <span class="mi">36</span><span class="p">,</span> <span class="nx">color</span><span class="o">:</span> <span class="s2">&#34;#42a5f5&#34;</span> <span class="p">};</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">count</span> <span class="o">&lt;</span> <span class="mi">100</span><span class="p">)</span>  <span class="k">return</span> <span class="p">{</span> <span class="nx">size</span><span class="o">:</span> <span class="mi">48</span><span class="p">,</span> <span class="nx">color</span><span class="o">:</span> <span class="s2">&#34;#26a69a&#34;</span> <span class="p">};</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="nx">count</span> <span class="o">&lt;</span> <span class="mi">1000</span><span class="p">)</span> <span class="k">return</span> <span class="p">{</span> <span class="nx">size</span><span class="o">:</span> <span class="mi">60</span><span class="p">,</span> <span class="nx">color</span><span class="o">:</span> <span class="s2">&#34;#ff7043&#34;</span> <span class="p">};</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="p">{</span> <span class="nx">size</span><span class="o">:</span> <span class="mi">72</span><span class="p">,</span> <span class="nx">color</span><span class="o">:</span> <span class="s2">&#34;#d32f2f&#34;</span> <span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">Cluster</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">count</span> <span class="p">})</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="p">{</span> <span class="nx">size</span><span class="p">,</span> <span class="nx">color</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">getStyle</span><span class="p">(</span><span class="nx">count</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="p">&lt;</span><span class="nt">ClusterBubble</span> <span class="na">size</span><span class="o">=</span><span class="p">{</span><span class="nx">size</span><span class="p">}</span> <span class="na">color</span><span class="o">=</span><span class="p">{</span><span class="nx">color</span><span class="p">}&gt;{</span><span class="nx">count</span><span class="p">}&lt;/</span><span class="nt">ClusterBubble</span><span class="p">&gt;;</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="k">default</span> <span class="nx">Cluster</span><span class="p">;</span>
</span></span></code></pre></div><h3 id="3-markerjs">3. <code>Marker.js</code></h3>
<p>A pin that opens an info window on click. The selected state comes from Redux so the rest of the app can react to it.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-jsx" data-lang="jsx"><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">React</span> <span class="nx">from</span> <span class="s2">&#34;react&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">useDispatch</span><span class="p">,</span> <span class="nx">useSelector</span> <span class="p">}</span> <span class="nx">from</span> <span class="s2">&#34;react-redux&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">Popper</span> <span class="p">}</span> <span class="nx">from</span> <span class="s2">&#34;@mui/material&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">styled</span> <span class="nx">from</span> <span class="s2">&#34;styled-components&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">selectMarker</span> <span class="p">}</span> <span class="nx">from</span> <span class="s2">&#34;./store/mapSlice&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">Pin</span> <span class="o">=</span> <span class="nx">styled</span><span class="p">.</span><span class="nx">div</span><span class="sb">`
</span></span></span><span class="line"><span class="cl"><span class="sb">  width: 24px;
</span></span></span><span class="line"><span class="cl"><span class="sb">  height: 24px;
</span></span></span><span class="line"><span class="cl"><span class="sb">  border-radius: 50%;
</span></span></span><span class="line"><span class="cl"><span class="sb">  background: #1976d2;
</span></span></span><span class="line"><span class="cl"><span class="sb">  border: 3px solid #fff;
</span></span></span><span class="line"><span class="cl"><span class="sb">  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
</span></span></span><span class="line"><span class="cl"><span class="sb">  cursor: pointer;
</span></span></span><span class="line"><span class="cl"><span class="sb">  transform: translate(-50%, -50%);
</span></span></span><span class="line"><span class="cl"><span class="sb">`</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">Marker</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">point</span> <span class="p">})</span> <span class="p">=&gt;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">dispatch</span> <span class="o">=</span> <span class="nx">useDispatch</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">selectedId</span> <span class="o">=</span> <span class="nx">useSelector</span><span class="p">((</span><span class="nx">s</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="nx">s</span><span class="p">.</span><span class="nx">map</span><span class="p">.</span><span class="nx">selectedId</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">anchorRef</span> <span class="o">=</span> <span class="nx">React</span><span class="p">.</span><span class="nx">useRef</span><span class="p">(</span><span class="kc">null</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="kr">const</span> <span class="nx">open</span> <span class="o">=</span> <span class="nx">selectedId</span> <span class="o">===</span> <span class="nx">point</span><span class="p">.</span><span class="nx">id</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">&lt;</span><span class="nt">Pin</span> <span class="na">ref</span><span class="o">=</span><span class="p">{</span><span class="nx">anchorRef</span><span class="p">}</span> <span class="na">onClick</span><span class="o">=</span><span class="p">{()</span> <span class="p">=&gt;</span> <span class="nx">dispatch</span><span class="p">(</span><span class="nx">selectMarker</span><span class="p">(</span><span class="nx">point</span><span class="p">.</span><span class="nx">id</span><span class="p">))}</span> <span class="p">/&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">&lt;</span><span class="nt">Popper</span> <span class="na">open</span><span class="o">=</span><span class="p">{</span><span class="nx">open</span><span class="p">}</span> <span class="na">anchorEl</span><span class="o">=</span><span class="p">{</span><span class="nx">anchorRef</span><span class="p">.</span><span class="nx">current</span><span class="p">}</span> <span class="na">placement</span><span class="o">=</span><span class="s">&#34;top&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="p">&lt;</span><span class="nt">InfoWindow</span> <span class="na">point</span><span class="o">=</span><span class="p">{</span><span class="nx">point</span><span class="p">}</span> <span class="p">/&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="p">&lt;/</span><span class="nt">Popper</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">&lt;/&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="k">default</span> <span class="nx">Marker</span><span class="p">;</span>
</span></span></code></pre></div><p>That&rsquo;s the easy part. Now the parts that actually cost me time.</p>
<h2 id="five-gotchas-that-ate-my-weeks">Five gotchas that ate my weeks</h2>
<h3 id="1-keeping-the-selected-marker-in-sync-across-three-views">1. Keeping the selected marker in sync across three views</h3>
<p>Three things need to know which marker is currently selected: the marker itself (to show its info window), the side list view (to highlight the row), and a &ldquo;view on map&rdquo; button (to fly to it).</p>
<p>I started by lifting state to the map component. That worked for about a week. Then I added the list. Then I wanted the URL to reflect the selection. By the third feature my map component was a state-management dumpster fire.</p>
<p>Moved it into Redux. Every component subscribes to <code>state.map.selectedId</code> and dispatches <code>selectMarker(id)</code>. Boring solution. Boring is what you want in production.</p>
<h3 id="2-cluster-bubbles-that-dont-scale-with-density">2. Cluster bubbles that don&rsquo;t scale with density</h3>
<p>A cluster wrapping 5 markers shouldn&rsquo;t look the same as one wrapping 500. Obvious in hindsight, but easy to forget when you&rsquo;re sprinting through layout.</p>
<p>I broke <code>getStyle()</code> into four buckets (10, 100, 1000, and 1000+). Different sizes, different colors. Ten minutes of work, biggest visible improvement of the whole project.</p>
<h3 id="3-info-window-broke-in-fullscreen-mode">3. Info window broke in fullscreen mode</h3>
<p>This one cost me a Friday afternoon and most of a Saturday morning.</p>
<p>Material-UI&rsquo;s <code>Popper</code> worked fine. Until someone clicked Google Maps&rsquo; fullscreen button. The popper either rendered offscreen or vanished behind the map. The reason: fullscreen mode moves the map element into a separate stacking context. My popper was anchored to a DOM node that wasn&rsquo;t in that tree anymore.</p>
<p>The fix was to listen for <code>fullscreenchange</code> on <code>document</code>, swap the container ref to the map&rsquo;s fullscreen container, and re-anchor the popper. Combined with <code>styled-components</code> for the popper styling, it became reliable. Not elegant. It works.</p>
<h3 id="4-latlngbounds-drifts-on-wide-horizontal-pans">4. <code>LatLngBounds</code> drifts on wide horizontal pans</h3>
<p>Pan east or west across a wide longitudinal span and the bounds returned by <code>google-map-react</code>&rsquo;s <code>onChange</code> callback don&rsquo;t match what&rsquo;s on screen. Cross the 180° meridian and it gets worse. Clustering then runs against the wrong bounds, and markers vanish or duplicate near the edges of the map.</p>
<p>I trusted the callback for too long. Once I stopped, the fix was straightforward: recompute the visible bounds from the map&rsquo;s center, viewport pixel dimensions, and zoom level. A few lines of geometry. The disappearing-marker bug was gone.</p>
<h3 id="5-grey-tiles-when-panning-vertically-past-the-world">5. Grey tiles when panning vertically past the world</h3>
<p>Pan far enough north or south and you see grey. Google Maps doesn&rsquo;t wrap vertically.</p>
<p>I wrote a wrapper around the pan handler to clamp lat values. Forty lines, edge cases, the whole thing. Then I found the built-in option, deleted my wrapper, and felt mildly stupid:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="cl"><span class="nx">options</span><span class="o">=</span><span class="p">{{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">restriction</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">latLngBounds</span><span class="o">:</span> <span class="p">{</span> <span class="nx">north</span><span class="o">:</span> <span class="mi">85</span><span class="p">,</span> <span class="nx">south</span><span class="o">:</span> <span class="o">-</span><span class="mi">85</span><span class="p">,</span> <span class="nx">west</span><span class="o">:</span> <span class="o">-</span><span class="mi">180</span><span class="p">,</span> <span class="nx">east</span><span class="o">:</span> <span class="mi">180</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="nx">strictBounds</span><span class="o">:</span> <span class="kc">true</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">},</span>
</span></span><span class="line"><span class="cl"><span class="p">}}</span>
</span></span></code></pre></div><p>Should&rsquo;ve read the docs first.</p>
<h2 id="what-id-use-in-2026">What I&rsquo;d use in 2026</h2>
<p>A lot has changed in five years. If I were writing this article fresh today, I&rsquo;d build the same three-component pattern (Map, Cluster, Marker) but with a different set of libraries underneath.</p>
<h3 id="the-google-maps-side">The Google Maps side</h3>
<p><a href="https://www.npmjs.com/package/google-map-react"><code>google-map-react</code></a> isn&rsquo;t actively maintained anymore. The active community has moved to <a href="https://visgl.github.io/react-google-maps/"><code>@vis.gl/react-google-maps</code></a>, built by the team that also makes deck.gl and react-map-gl. That&rsquo;s the default Google Maps + React choice in 2026.</p>
<p>The big win: <code>AdvancedMarker</code> lets you render React directly inside the map without the portal acrobatics I needed for the fullscreen popper bug. If I were rebuilding today, gotcha #3 would mostly disappear.</p>
<h3 id="clustering">Clustering</h3>
<p>You no longer need to wire up clustering yourself. Two clean options:</p>
<ul>
<li><a href="https://www.npmjs.com/package/@googlemaps/markerclusterer"><code>@googlemaps/markerclusterer</code></a> is the official library, maintained by Google. Works cleanly with <code>@vis.gl/react-google-maps</code>. Under the hood it uses <a href="https://github.com/mapbox/supercluster">supercluster</a>, the same algorithm <code>points-cluster</code> was based on.</li>
<li><a href="https://github.com/mapbox/supercluster"><code>supercluster</code></a> directly if you want full control over rendering, like I did here. Written by Vladimir Agafonkin at Mapbox. Still the fastest JS clustering algorithm I know of, and it powers basically every modern map clustering library.</li>
</ul>
<h3 id="if-you-dont-actually-need-google-maps">If you don&rsquo;t actually need Google Maps</h3>
<p>Worth asking. Google Maps has gotten expensive, and there are good alternatives:</p>
<ul>
<li><a href="https://visgl.github.io/react-map-gl/"><code>react-map-gl</code></a> with <a href="https://maplibre.org/projects/gl-js/">MapLibre GL JS</a> is the open-source path. MapLibre is a community fork of Mapbox GL JS that no longer ties you to Mapbox&rsquo;s pricing or tiles. Production-ready in 2026. Same declarative React API.</li>
<li>Pair it with <a href="https://maplibre.org/maplibre-gl-js/docs/guides/large-data/">Mapbox&rsquo;s vector tile clustering</a> (it&rsquo;s a first-class GeoJSON source option, not a separate library) and you get clustering for free.</li>
</ul>
<h3 id="for-really-large-datasets">For really large datasets</h3>
<p>If you&rsquo;re rendering hundreds of thousands of points and clustering isn&rsquo;t enough, look at <a href="https://deck.gl">deck.gl</a>. Its layer system (heatmap, hex grid, scatterplot) running on top of a base map handles datasets that would crush any pin-marker approach. WebGL-backed, runs on the GPU. Same team behind <code>@vis.gl/react-google-maps</code> and <code>react-map-gl</code>, so it integrates cleanly.</p>
<h3 id="what-about-the-five-gotchas">What about the five gotchas?</h3>
<ul>
<li><strong>Selected marker state in Redux:</strong> still valid. Redux Toolkit makes it cleaner now, but the pattern is the same. Zustand or Jotai would also work fine here.</li>
<li><strong>Density-scaled cluster bubbles:</strong> still valid. <code>@googlemaps/markerclusterer</code> supports custom renderers; you&rsquo;d pass the same <code>getStyle()</code> logic.</li>
<li><strong>Fullscreen popper:</strong> mostly solved by <code>AdvancedMarker</code>, which renders inside the map&rsquo;s DOM tree by design.</li>
<li><strong>Bounds drift:</strong> still real on edge cases, but <code>@vis.gl/react-google-maps</code> exposes the map instance cleanly, so you can call <code>map.getBounds()</code> directly instead of trusting the change callback.</li>
<li><strong>Grey vertical tiles:</strong> same <code>restriction</code> option, same fix. Still right.</li>
</ul>
<h2 id="wrap-up">Wrap-up</h2>
<p>The article&rsquo;s stack is dated, but the architecture isn&rsquo;t. Three components, Redux for selection, supercluster (or its derivatives) for the math, and a handful of real-world bugs that don&rsquo;t go away just because you switch libraries.</p>
<p>If you&rsquo;ve solved any of these differently, especially the fullscreen popper or the bounds drift, I&rsquo;d love to hear how. Drop a comment or <a href="/contact/">reach out</a>.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
