# leaflet: React Leaflet

這次用React開發地圖應用程式不是使用原生的Leaflet，而使用到的套件有：
- [react-leaflet](https://react-leaflet.js.org/)
- [react-leaflet-cluster](https://github.com/akursat/react-leaflet-cluster)

這次使用別人寫好的[React Leaflet](https://react-leaflet.js.org/)套件，而不是用[原生的Leaflet](https://leafletjs.com/)。

其實也是可用原生的leaflet去寫，寫法可以參考這篇 - [Using Leaflet.js in a React Project: Build a Mapping Application](https://javascript.plainenglish.io/using-leaflet-js-in-a-react-project-build-a-mapping-application-f97aeef25c33)，但忘記在哪一篇問答看到建議使用React版套件，所以還是用了寫好的[React Leaflet](https://react-leaflet.js.org/)套件，然後踩了一堆坑。

-----
### 灰色底圖
![](https://i.imgur.com/6R7FG9Y.png)
這個狀態視窗改變大小之後，原本未顯示在可見視窗內的底圖會變成灰色，搜尋網路半天、參考了幾篇問答：
- [react-leaflet map not correctly displayed](https://stackoverflow.com/questions/40365440/react-leaflet-map-not-correctly-displayed)
- [Map is not visible at initialization using react-leaflet](https://stackoverflow.com/questions/59856361/map-is-not-visible-at-initialization-using-react-leaflet)
- [Leaflet Map not showing in bootstrap div](https://stackoverflow.com/questions/31030949/leaflet-map-not-showing-in-bootstrap-div?rq=1)
- [React Leaflet - TileLayer does not render when it is first loaded](https://stackoverflow.com/questions/65286018/react-leaflet-tilelayer-does-not-render-when-it-is-first-loaded)
- [Next.js React Leaflet Map not Showing Properly](https://stackoverflow.com/questions/68267395/next-js-react-leaflet-map-not-showing-properly)
- [Map size not invalidated on height / width change](https://github.com/PaulLeCam/react-leaflet/issues/340)

統整以上問答內的資訊，大概得出幾個解法：
1. `import "leaflet/dist/leaflet.css";`
2. 給予width、height
這個解法要分別在`.leaflet-containter` 和 `<MapContainer></MapContainer>` 寫上寬高：
    ```
    // .css
     .leaflet-container {
      width: 100vw; 
      height: 100vh;
    }

     // app.jsx
     import {MapContanter} from 'react-leaflet';

     export default function App(){

         return (
             <MapContainer
                 // style這行
                 style={{ height: '100vh', width: '100wh' }}
             >
                 ...
             </MapContainer>
         );

     }
    ```
3. `map.invalidateSize()`
若是使用[React Leaflet](https://react-leaflet.js.org/)套件的正規寫法如下：
    ```
    const map = useMap()

    useEffect(() => {
        setTimeout(() => { 
            map.invalidateSize(); 
        }, 250); 
    }, [map])
    ```

前兩個解法其實[React Leaflet文件上也有寫](https://react-leaflet.js.org/docs/start-setup/)，有些stackoverflow解答會說只要給高度 `100%` 或 `100vh`，但因為專案在電腦版和手機版都要做響應式設計，所以最後是用 `style={{ height: '100vh', width: '100vw' }}` 解決，而且最好要包一個親層元件(parent component)，讓地圖只在親層元件範圍內有響應式的設計，不會佔用到親層元件以外的版面。

此外，以地圖寬度的 `100vw` 為例，若地圖外圍包了一個親層元件，事實上子層(child element)的地圖並不會置中，**若想要置中寬度為100vw的子層元素，要在子層元素的CSS再加上：**
```
position: relative;
left: 50%;
transform: translateX(-50%);
```

也就是說地圖的CSS還得這樣修改：
 ```
 // app.jsx
 import {MapContanter} from 'react-leaflet';

 export default function App(){


     return (
         <MapContainer
             // style這行
             style={{ 
                 height: '100vh',
                 width: '100wh',
                 position: 'relative',
                 left: '50%',
                 transform: 'translateX(-50%)'
             }}
         >
             ...
         </MapContainer>
     );

 }
 ```

置中子層的地圖元素的目的是為了**避免設置地圖中心會有位移的狀況發生**
<br/>

不過寫上 `style={{ height: '100vh', width: '100vw' }}` 的缺點是，`<MapContainer>` 元件內的子層元件(child components)，除了圖層可用滑鼠平移滑動外，其他子層元件可能會超出視窗範圍，例如圖中紅色區域的地圖要素不在黑框代表的視窗內：
![](https://i.imgur.com/1ri9ai7.png)

如果要讓紅色區域的地圖要素浮動在藍色區域的響應式地圖上方，應該要把地圖要素抽離 `<MapContainer>` 以外，但如果地圖要素會用到拿來存取地圖的 `const map = useMap` hook，譬如 `map.zoomIn()`、`map.setView()` 等，這個 `useMap` hook只限定在  `<MapContainer>` 以內使用，所以必須用其他方式來使用leaflet存取地圖的方法。
<br/><br/>

-----
### 存取地圖方法
前一個問題是要怎麼在 `<MapContainer>` 組件以外的地方存取像是 `zoomIn()`、`setView()` 這些地圖的方法，參考以下兩篇的作法：
- [How to call useMap() outside of the file where <MapContainer> is NOT called?](https://stackoverflow.com/questions/71104753/how-to-call-usemap-outside-of-the-file-where-mapcontainer-is-not-called)
- [React + leaflet + routing machine : Problem accessing map outside MapContainer](https://stackoverflow.com/questions/71516019/react-leaflet-routing-machine-problem-accessing-map-outside-mapcontainer)

因為還要配合專案全域(global)使用這些地圖方法的需求，最後我是在context裡寫了一個 `const mapRef = useRef(null)`，然後：
```
// app.jsx
import {useContext} from 'react';
import {MapContext} from 'store';
import {MapContanter} from 'react-leaflet';

export default function App(){
    const mapContext = useContext(MapContext);   

    return (
     <MapContainer
         ref={mapContext.mapRef}
         style={{ height: '100vh', width: '100wh' }}
     >
         ...
     </MapContainer>
    );

}
```

React全域狀態context的寫法可參考React新文件 ─ [Scaling Up with Reducer and Context](https://react.dev/learn/scaling-up-with-reducer-and-context)。
<br/>

使用時跟一般的ref一樣：

```
import {useContext} from 'react';
import {MapContext} from 'store';
import * as L from 'leaflet';

export default function ZoomInButton(){
    const mapContext = useContext(MapContext); 
    
    return <button
            onClick={
                () => (mapContext.mapRef as unknown as React.MutableRefObject<L.Map>).current.zoomIn()
            }
            >放大地圖</button>
}
```

<br/><br/>

-----
### 空白標籤
![](https://i.imgur.com/pTTgRzt.png)
如圖，專案已經用了boolean型別的state來控制 `<Tooltip>` 的顯示與否，例如：
```
import {useState} from 'react';

export default function Map(){
    const [isShow, setIsShow] = useState(false);

    return(
        <LayerGaroup>
            {markers.map(marker=>
                <Marker>
                    {isShow && <Tooltip>{marker.title}</Tooltip>}
                </Marker>
            )}
        </LayerGaroup>
    )
}
```

但是在state等於 `false` 時地圖上的 `<Tooltip>` 卻沒有完全消失。一開始有想到可能是沒有給key的問題，但發現React會render兩次，所以會出現key重複出現的錯誤。後來看了以下類似案例是用uuid解決，給定唯一的key值確實也解決出現空白標籤的問題：

- [Empty ghost Popup when dynamically rendering popup in react-leaflet](https://stackoverflow.com/questions/65151457/empty-ghost-popup-when-dynamically-rendering-popup-in-react-leaflet)
- [Empty ghost popup left over when dynamically rendering marker popup #832](https://github.com/PaulLeCam/react-leaflet/issues/832)
<br/><br/><br/>

-----
#### References
- [Using Leaflet.js in a React Project: Build a Mapping Application](https://javascript.plainenglish.io/using-leaflet-js-in-a-react-project-build-a-mapping-application-f97aeef25c33)
- [Tutorial: React ⚛ + Leaflet 🗺](https://dev.to/maj07/tutorial-react-leaflet-d65)
- [how to set child Div with 100vw of device width when parent div having width :80vw](https://stackoverflow.com/questions/45371811/how-to-set-child-div-with-100vw-of-device-width-when-parent-div-having-width-80)
