leaflet: React Leaflet
這次用React開發地圖應用程式不是使用原生的Leaflet,而使用到的套件有:
這次使用別人寫好的React Leaflet套件,而不是用原生的Leaflet。
其實也是可用原生的leaflet去寫,寫法可以參考這篇 - Using Leaflet.js in a React Project: Build a Mapping Application,但忘記在哪一篇問答看到建議使用React版套件,所以還是用了寫好的React Leaflet套件,然後踩了一堆坑。
灰色底圖
這個狀態視窗改變大小之後,原本未顯示在可見視窗內的底圖會變成灰色,搜尋網路半天、參考了幾篇問答:
- react-leaflet map not correctly displayed
- Map is not visible at initialization using react-leaflet
- Leaflet Map not showing in bootstrap div
- React Leaflet - TileLayer does not render when it is first loaded
- Next.js React Leaflet Map not Showing Properly
- Map size not invalidated on height / width change
統整以上問答內的資訊,大概得出幾個解法:
import "leaflet/dist/leaflet.css";
給予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> ); }
map.invalidateSize()
若是使用React Leaflet套件的正規寫法如下:const map = useMap() useEffect(() => { setTimeout(() => { map.invalidateSize(); }, 250); }, [map])
前兩個解法其實React Leaflet文件上也有寫,有些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>
);
}
置中子層的地圖元素的目的是為了避免設置地圖中心會有位移的狀況發生
不過寫上 style={{ height: '100vh', width: '100vw' }}
的缺點是,<MapContainer>
元件內的子層元件(child components),除了圖層可用滑鼠平移滑動外,其他子層元件可能會超出視窗範圍,例如圖中紅色區域的地圖要素不在黑框代表的視窗內:
如果要讓紅色區域的地圖要素浮動在藍色區域的響應式地圖上方,應該要把地圖要素抽離 <MapContainer>
以外,但如果地圖要素會用到拿來存取地圖的 const map = useMap
hook,譬如 map.zoomIn()
、map.setView()
等,這個 useMap
hook只限定在 <MapContainer>
以內使用,所以必須用其他方式來使用leaflet存取地圖的方法。
存取地圖方法
前一個問題是要怎麼在 <MapContainer>
組件以外的地方存取像是 zoomIn()
、setView()
這些地圖的方法,參考以下兩篇的作法:
- How to call useMap() outside of the file where is NOT called?
- 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。
使用時跟一般的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>
}
空白標籤
如圖,專案已經用了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
- Empty ghost popup left over when dynamically rendering marker popup #832