React-Leaflet LocateControl component - keeps duplicating on each refresh - javascript

I'm using Locate Control in React Leaflet, but the Locate Control buttons are always duplicated, and sometimes I get 3 or 4 of them (see image below). I'm running the function through a useEffect with empty dependency to only fire it once, but no matter. I can target the class with display: none, but then both disappear. I feel like this might be an issue with Locate Control library? Really not sure. Open to any help or ideas.
import { useEffect } from "react"
import { useMap } from "react-leaflet"
import Locate from "leaflet.locatecontrol"
import "leaflet.locatecontrol/dist/L.Control.Locate.min.css"
const AddLocate = () => {
const map = useMap()
useEffect(() => {
const locateOptions = {
position: "bottomleft",
flyTo: true,
}
const locateControl = new Locate(locateOptions)
locateControl.addTo(map)
}, [])
return null
}
export default AddLocate;

Looks like you use a package made for leaflet. Which should for the most parts be okay. However the way you add the control is not really the react-leaflet way, where we want to add add components rather than add "stuff" directly to the map.
Below you can see how easy it is to implement a location component that you simply just can add as component within your MapContainer.
import { ActionIcon } from "#mantine/core";
import React, { useState } from "react";
import { useMapEvents } from "react-leaflet";
import { CurrentLocation } from "tabler-icons-react";
import LeafletControl from "./LeafletControl";
interface LeafletMyPositionProps {
zoom?: number;
}
const LeafletMyPosition: React.FC<LeafletMyPositionProps> = ({ zoom = 17 }) => {
const [loading, setLoading] = useState<boolean>(false);
const map = useMapEvents({
locationfound(e) {
map.flyTo(e.latlng, zoom);
setLoading(false);
},
});
return (
<LeafletControl position={"bottomright"}>
<ActionIcon
onClick={() => {
setLoading(true);
map.locate();
}}
loading={loading}
variant={"transparent"}
>
<CurrentLocation />
</ActionIcon>
</LeafletControl>
);
};
export default LeafletMyPosition;
And for LeafletControl I just have this reusable component:
import L from "leaflet";
import React, { useEffect, useRef } from "react";
const ControlClasses = {
bottomleft: "leaflet-bottom leaflet-left",
bottomright: "leaflet-bottom leaflet-right",
topleft: "leaflet-top leaflet-left",
topright: "leaflet-top leaflet-right",
};
type ControlPosition = keyof typeof ControlClasses;
export interface LeafLetControlProps {
position?: ControlPosition;
children?: React.ReactNode;
}
const LeafletControl: React.FC<LeafLetControlProps> = ({
position,
children,
}) => {
const divRef = useRef(null);
useEffect(() => {
if (divRef.current) {
L.DomEvent.disableClickPropagation(divRef.current);
L.DomEvent.disableScrollPropagation(divRef.current);
}
});
return (
<div ref={divRef} className={position && ControlClasses[position]}>
<div className={"leaflet-control"}>{children}</div>
</div>
);
};
export default LeafletControl;

I would do some debugging to that useEffect to see if it's only happening once. It's possible the entire component is mounted multiple times.

Related

React elements are removed on state update

I'm supposed to have a modal appear with an image in it. There are next and previous buttons which controls which image you are currently viewing. The modal is rendered in a portal. That in itself is working correctly. However, when I add children, and those childrens are updated, the modal only (not the portal) gets removed from the flow. In the React DevTools, the "isOpen" state of the modal is still set to true. I am using React 17.0.2 with NextJS 12.0.4 and Styled Components 5.3.3.
I have tried:
memoizing my components (as you can see there are some remnants of those trials) but this did not work
extracting the state of the modal to the parent and passing it as props and it didn't work either
I know there must be something wrong that I'm doing here so if you could help me find it that would be much appreciated!
Here is the controller where the modal is rendered:
import { FC, MouseEventHandler, useEffect, useState } from "react";
import { Photo } from "services/Images/Images.interfaces";
import { useGetNextPhoto, useGetPhotos, useGetPreviousPhoto } from "state";
import SlideshowContextProvider from "./Context/SlideshowContext";
import SlideShowModal from "./SlideShowModal";
const SlideshowController: FC = () => {
const photos = useGetPhotos();
const [currentlyViewedPhoto, setCurrentlyViewedPhoto] = useState<Photo | null>(null);
const nextPhoto = useGetNextPhoto(currentlyViewedPhoto?.id);
const previousPhoto = useGetPreviousPhoto(currentlyViewedPhoto?.id);
const onPreviousRequest: MouseEventHandler<HTMLButtonElement> = (event) => {
event.preventDefault();
setCurrentlyViewedPhoto(previousPhoto);
};
const onNextRequest: MouseEventHandler<HTMLButtonElement> = async (event) => {
event.preventDefault();
setCurrentlyViewedPhoto(nextPhoto);
};
useEffect(() => {
setCurrentlyViewedPhoto(photos[0]);
}, [photos]);
return (
<SlideshowContextProvider
currentlyViewing={currentlyViewedPhoto}
onNextSlideRequest={onNextRequest}
onPreviousSlideRequest={onPreviousRequest}
>
<SlideShowModal />
</SlideshowContextProvider>
);
};
export default SlideshowController;
The SlideshowModal:
import { Modal } from "components";
import { FC } from "react";
import SlideshowControlBar from "./SlideshowControlBar";
import SlideshowImage from "./SlideshowImage";
const SlideShowModal: FC = () => {
return (
<Modal uniqueKey="slideshow">
<SlideshowImage />
<SlideshowControlBar />
</Modal>
);
};
export default SlideShowModal;
The modal in itself:
import Portal from "components/Portal/Portal";
import { FC, useEffect, useMemo, useState } from "react";
import { useRegisterModal } from "state";
import styled from "styled-components";
import useWindowScrollLock from "./hook/UseWindowScrollLock";
interface Props {
uniqueKey: string;
isBackgroundOpaque?: boolean;
}
... Styled elements
const Modal: FC<Props> = ({ uniqueKey, isBackgroundOpaque = true, children }) => {
const [isOpen, setIsOpen] = useState(false);
const open = () => setIsOpen(true);
const close = () => setIsOpen(false);
const register = useRegisterModal(uniqueKey);
const isModalOpen = useMemo(() => isOpen, [isOpen]);
useEffect(() => {
register({ open, close });
}, [register]);
useWindowScrollLock(isModalOpen);
return isModalOpen ? (
<Portal>
<Container>
<InnerModal>
<Close onClick={close}>X</Close>
{children}
</InnerModal>
</Container>
<Background onClick={close} opaque={isBackgroundOpaque} />
</Portal>
) : null;
};
export default Modal;

React useEffect and React-Query useQuery issue?

I'm still new to React so forgive me if this is a silly approach to this problem.
My goal: Global error handling using a context provider and a custom hook.
The Problem: I can't remove errors without them immediately being re-added.
I display my errors via this component in the shell...
import React, { useState, useEffect } from 'react'
import Alert from '#mui/material/Alert'
import Collapse from '#mui/material/Collapse'
import { useAlertContext } from '#/context/alert-context/alert-context'
export default function AppAlert () {
const [show, setShow] = useState(false)
const alertContext = useAlertContext()
const handleClose = () => {
alertContext.remove()
setShow(false)
}
useEffect(() => {
if (alertContext.alert) {
setShow(true)
}
}, [alertContext.alert])
return (
<Collapse in={show}>
<Alert severity='error' onClose={handleClose}>
{alertContext.alert}
</Alert>
</Collapse>
)
}
I have a provider setup that also exposes a custom hook...
import React, { useState, createContext, useContext } from 'react'
const AlertContext = createContext()
const AlertProvider = ({ children }) => {
const [alert, setAlert] = useState(null)
const removeAlert = () => setAlert(null)
const addAlert = (message) => setAlert(message)
return (
<AlertContext.Provider value={{
alert,
add: addAlert,
remove: removeAlert
}}
>
{children}
</AlertContext.Provider>
)
}
const useAlertContext = () => {
return useContext(AlertContext)
}
export {
AlertProvider as default,
useAlertContext
}
And finally I have a hook setup to hit an API and call throw errors if it any occur while fetching the data. I'm purposely triggering a 404 by passing a bad API path.
import { useEffect } from 'react'
import { useQuery } from 'react-query'
import ApiV4 from '#/services/api/v4/base'
import { useAlertContext } from '#/context/alert-context/alert-context'
export const useAccess = () => {
const alertContext = useAlertContext()
const route = '/accessx'
const query = useQuery(route, async () => await ApiV4.get(route), {
retry: 0
})
useEffect(() => {
if (query.isError) {
alertContext.add(query.error.toString())
}
}, [alertContext, query.isError, query.error])
return query
}
This code seems to be the issue. Because alertContext.remove() triggers useEffect here and query.error still exists, it immediately re-adds the error to the page on remove. Removing alertContext from the array works, but it is not a real fix and linter yells.
useEffect(() => {
if (query.isError) {
alertContext.add(query.error.toString())
}
}, [alertContext, query.isError, query.error])
This is a perfectly fine approach to the problem. You've also accurately identified the problem. The solution is to create a second hook with access to the methods that will modify the context. AppAlert needs access to the data in the context, and needs to update when AlertContext.alert changes. UseAccess only needs to be able to call AlertContext.add, and that method wont change and trigger a re-render. This can be done with a second Context. You can just expose one Provider and bake the actions provider into the outer context provider.
import React, { useState, createContext, useContext } from 'react'
const AlertContext = createContext()
const AlertContextActions = createContext()
const AlertProvider = ({ children }) => {
const [alert, setAlert] = useState(null)
const removeAlert = () => setAlert(null)
const addAlert = (message) => setAlert(message)
return (
<AlertContext.Provider value={{ alert }}>
<AlertContextActions.Provider value={{ addAlert, removeAlert }}>
{children}
</AlertContextActions.Provider>
</AlertContext.Provider>
)
}
const useAlertContext = () => {
return useContext(AlertContext)
}
export {
AlertProvider as default,
useAlertContext
}
Now, where you need access to the alert you use one hook and where you need access to the actions you use the other.
// in AppAlert
import { useAlertContext, useAlertContextActions } from '#/context/alert-context/alert-context'
...
const { alert } = useAlertContext()
const { removeAlert } = useAlertContextActions()
And finally
// in useAccess
import { useAlertContextActions } from '#/context/alert-context/alert-context'
...
const { addAlert } = useAlertContextActions()
So I found a solution that seems to work for my purposes. I got a hint from this article. https://mortenbarklund.com/blog/react-architecture-provider-pattern/
Note the use of useCallback above. It ensures minimal re-renders of components using this context, as the function is guaranteed to be stable (as its memoized without dependencies).
So with this I tried the following and it solved the problem.
import React, { useState, createContext, useContext, useCallback } from 'react'
const AlertContext = createContext()
const AlertProvider = ({ children }) => {
const [alert, setAlert] = useState(null)
const removeAlert = useCallback(() => setAlert(null), [])
const addAlert = useCallback((message) => setAlert(message), [])
return (
<AlertContext.Provider value={{
alert,
add: addAlert,
remove: removeAlert
}}
>
{children}
</AlertContext.Provider>
)
}
const useAlertContext = () => {
return useContext(AlertContext)
}
export {
AlertProvider as default,
useAlertContext
}
My goal: Global error handling
One problem with the above useEffect approach is that every invocation of useAccess will run their own effects. So if you have useAccess twice on the page, and it fails, you will get two alerts, so it's not really "global".
I would encourage you to look into the global callbacks on the QueryCache in react-query. They are made for this exact use-case: To globally handle errors. Note that to use context, you would need to create the queryClient inside the Application, and make it "stable" with either useRef or useState:
function App() {
const alertContext = useAlertContext()
const [queryClient] = React.useState(() => new QueryClient({
queryCache: new QueryCache({
onError: (error) =>
alertContext.add(error.toString())
}),
}))
return (
<QueryClientProvider client={queryClient}>
<RestOfMyApp />
</QueryClientProvider>
)
}
I also have some examples in my blog.

Combine hooks without using Provider

I'm trying to combine several contexts that are feed with some async operations, in my app's pages.
I would like to combine these contexts without using the Context.Provider because it could be verbose. For example,
<Route path="/discover">
<MainContainer extraClass="discover-container" hasHeader={true}>
<UserContext>
<ContentContextProvider>
<NotificationContext>
<Discover />
</NotificationContext>
</ContentContextProvider>
</UserContext>
</MainContainer>
</Route>
In each of these Context I wrapper the child with the context. Fe,
import React from "react";
import useAllContent from "utils/hooks/useAllContent";
const ContentContext = React.createContext({});
export const ContentContextProvider = ({ children }) => {
const { allContent, setAllContents } = useAllContent([]);
return (
<ContentContext.Provider value={{ allContent, setAllContents }}>
{children}
</ContentContext.Provider>
);
};
export default ContentContext;
This works, but as I mentioned before is very verbose so i would like to use the Contexts like an objetcs to combine between them.
I tried:
import { useState, useEffect, useCallback } from "react";
import { DataStore, Predicates } from "#aws-amplify/datastore";
import { Content } from "models";
const useAllContent = (initialValue) => {
const [allContent, setContent] = useState(initialValue);
const setAllContents = useCallback(async () => {
const contents = await DataStore.query(Content, Predicates.ALL);
setContent(contents);
}, []);
useEffect(() => {
if (allContent === 0) setAllContents();
}, [allContent, setAllContents]);
return { allContent, setAllContents };
};
export default useAllContent;
import React from "react";
import useAllContent from "utils/hooks/useAllContent";
const { allContent, setAllContents } = useAllContent([]);
const ContentContext = React.createContext({ allContent, setAllContents });
export default ContentContext;
But I break the rule × Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
How could i achieve it?
Combining instances of React.Context in the way you describe would require manipulating the values they contain. But there is no way to access the value of a Context without using its corresponding Provider. I sympathize with the dislike of verboseness, but it is unavoidable in this case.

Leaflet with next.js?

I am getting a ReferenceError:
window is not defined when using next.js with leaflet.js .
Wondering if there's a simple solution to this problem - is using next.js overcomplicating my workflow?
for those curious with the exact code,
import React, { createRef, Component } from "react";
import L from "leaflet";
import { Map, TileLayer, Marker, Popup, DivOverlay } from "react-leaflet";
import axios from "axios";
import Header from "./Header";
export default class PDXMap extends Component {
state = {
hasLocation: false,
latlng: {
lat: 45.5127,
lng: -122.679565
},
geoJSON: null
};
mapRef = createRef();
componentDidMount() {
this.addLegend();
if (!this.state.hasLocation) {
this.mapRef.current.leafletElement.locate({
setView: true
});
}
axios
.get(
"https://opendata.arcgis.com/datasets/40151125cedd49f09d211b48bb33f081_183.geojson"
)
.then(data => {
const geoJSONData = data.data;
this.setState({ geoJSON: geoJSONData });
return L.geoJSON(this.state.geoJSON).addTo(
this.mapRef.current.leafletElement
);
});
}
handleClick = () => {
this.mapRef.current.leafletElement.locate();
};
handleLocationFound = e => {
console.log(e);
this.setState({
hasLocation: true,
latlng: e.latlng
});
};
getGeoJsonStyle = (feature, layer) => {
return {
color: "#006400",
weight: 10,
opacity: 0.5
};
};
addLegend = () => {
const map = this.mapRef.current.leafletElement;
L.Control.Watermark = L.Control.extend({
onAdd: function(map) {
var img = L.DomUtil.create("img");
img.src = "https://leafletjs.com/docs/images/logo.png";
img.style.width = "200px";
return img;
}
});
L.control.watermark = function(opts) {
return new L.Control.Watermark(opts);
};
L.control.watermark({ position: "bottomleft" }).addTo(map);
};
render() {
const marker = this.state.hasLocation ? (
<Marker position={this.state.latlng}>
<Popup>
<span>You are here</span>
</Popup>
</Marker>
) : null;
return (
<Map
className="map-element"
center={this.state.latlng}
length={4}
onClick={this.handleClick}
setView={true}
onLocationfound={this.handleLocationFound}
ref={this.mapRef}
zoom={14}
>
<TileLayer
attribution='&copy OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{marker}
</Map>
);
}
}
/**
* TODO: Add Header + Legend to map
* - Header to be styled
* - Legend to be present in header
*
*/
import React from 'react';
import PDXMap from "../components/map";
export default function SignIn() {
const classes = useStyles();
return (
<PDXMap/>
);
}
I'm happy to use any way forward - just interested in getting a functional product.
Cheers!
Update
Hey everyone,
I am still getting this error (came back to this a bit later than I had planned haha).
I am currently using this approach with useEffects,
import React, {useEffect, useState} from 'react';
function RenderCompleted() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true)
return () => {
setMounted(false)
}
});
return mounted;
}
export default RenderCompleted;
and this is the page it is showing on
import React, { useEffect } from "react";
import Router, { useRouter } from "next/router";
import { useRef, useState } from "react";
//viz
import PDXMap from "../../components/Visualization/GIS/map";
import RenderCompleted from "../../components/utils/utils";
// import fetch from 'isomorphic-unfetch';
import { Cookies, CookiesProvider } from "react-cookie";
const cookies = new Cookies();
//containers
// Layouts
import Layout from "../../components/Layout/Layout_example";
import Chart from "../../components/Visualization/Graphs/Chart";
import Table from "../../components/Visualization/Tables/Table";
import Sidebar from "../../components/Layout/Sidebar/SidebarProperty";
export default function Bargains() {
// const [inbrowser, setBrowser] = useState(false);
const choiceRef = useRef<any>();
const [message, setMessage] = useState<any>(null);
const [productList, setProductList] = useState<any>([]);
const [searched, setSearched] = useState(false);
const router = useRouter();
let token = cookies.get("token");
// useEffect(() => {
// setBrowser(true);
// });
const isMounted = RenderCompleted();
const columns = React.useMemo(
() => [
....
],
[]
)
async function handleChoice() {
console.log("searching...", choiceRef.current?.value);
setMessage("Searching...");
var headers = {
"Content-Type": "application/x-www-form-urlencoded",
"auth-token": token,
};
fetch(
....
}
<div className="flex flex-wrap ">
{isMounted && <PDXMap/>}
<Table columns={columns as any} data={productList as any} />
</div>
</div>
</div>
</div>
</Layout>
)
}
With the same error message of
ReferenceError: window is not defined
##update two
Okay, so oddly, it does work when I browse into the site from another page, but not when i load the page itself.
Will have a think on this, but perhaps it is because the map is loading data with componentDidMount() and that is interacting weirdly?
Update
Okay I've created a more simple example based on https://github.com/rajeshdh/react-leaflet-with-nextjs
Now it is loading, but the tiles are showing incorrectly, with some tiles not loading.
This is the map component I am using to be simple,
import React, { Component, createRef } from 'react';
import { Map, TileLayer, Marker, Popup, MapControl, withLeaflet } from 'react-leaflet';
import { GeoSearchControl, OpenStreetMapProvider } from 'leaflet-geosearch';
class SearchBox extends MapControl {
constructor(props) {
super(props);
props.leaflet.map.on('geosearch/showlocation', (e) => props.updateMarker(e));
}
createLeafletElement() {
const searchEl = GeoSearchControl({
provider: new OpenStreetMapProvider(),
style: 'bar',
showMarker: true,
showPopup: false,
autoClose: true,
retainZoomLevel: false,
animateZoom: true,
keepResult: false,
searchLabel: 'search'
});
return searchEl;
}
}
export default class MyMap extends Component {
state = {
center: {
lat: 31.698956,
lng: 76.732407,
},
marker: {
lat: 31.698956,
lng: 76.732407,
},
zoom: 13,
draggable: true,
}
refmarker = createRef(this.state.marker)
toggleDraggable = () => {
this.setState({ draggable: !this.state.draggable });
}
updateMarker = (e) => {
// const marker = e.marker;
this.setState({
marker: e.marker.getLatLng(),
});
console.log(e.marker.getLatLng());
}
updatePosition = () => {
const marker = this.refmarker.current;
if (marker != null) {
this.setState({
marker: marker.leafletElement.getLatLng(),
});
}
console.log(marker.leafletElement.getLatLng());
}
render() {
const position = [this.state.center.lat, this.state.center.lng];
const markerPosition = [this.state.marker.lat, this.state.marker.lng];
const SearchBar = withLeaflet(SearchBox);
return (
<div className="map-root">
<Map center={position} zoom={this.state.zoom} style={{
height:"700px"
}}>
<TileLayer
attribution='&copy OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<Marker
draggable={true}
onDragend={this.updatePosition}
position={markerPosition}
animate={true}
ref={this.refmarker}>
<Popup minWidth={90}>
<span onClick={this.toggleDraggable}>
{this.state.draggable ? 'DRAG MARKER' : 'MARKER FIXED'}
</span>
</Popup>
</Marker>
<SearchBar updateMarker={this.updateMarker} />
</Map>
<style jsx>{`
.map-root {
height: 100%;
}
.leaflet-container {
height: 400px !important;
width: 80%;
margin: 0 auto;
}
`}
</style>
</div>
);
}
}
And to call it I am using this,
const SimpleExample = dynamic(() => import("../../components/Visualization/GIS/map"), {
ssr: false
});
And have tried this
{isMounted && }
Answer for 2020
I also had this problem and solved it in my own project, so I thought I would share what I did.
NextJS can dynamically load libraries and restrict that event so it doesn't happen during the server side render. See the documentation for more details.
In my examples below I will use and modify example code from the documentation websites of both NextJS 10.0 and React-Leaflet 3.0.
Side note: if you use TypeScript, make sure you install #types/leaflet because otherwise you'll get compile errors on the center and attribution attributes.
To start, I split my react-leaflet code out into a separate component file like this:
import { MapContainer, Marker, Popup, TileLayer } from 'react-leaflet'
import 'leaflet/dist/leaflet.css'
const Map = () => {
return (
<MapContainer center={[51.505, -0.09]} zoom={13} scrollWheelZoom={false} style={{height: 400, width: "100%"}}>
<TileLayer
attribution='© OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<Marker position={[51.505, -0.09]}>
<Popup>
A pretty CSS3 popup. <br /> Easily customizable.
</Popup>
</Marker>
</MapContainer>
)
}
export default Map
I called that file map.tsx and placed it in a folder called components but you might call it map.jsx if you don't use TypeScript.
Note: It is important that this code is in a separate file from where it is embedded into your page because otherwise you'll still get window undefined errors.
Also Note: don't forget to specify the style of the MapContainer component so it doesn't render as zero pixels in height/width. In the above example, I added the attribute style={{height: 400, width: "100%"}} for this purpose.
Now to use that component, take advantage of NextJS's dynamic loading like this:
import dynamic from 'next/dynamic'
function HomePage() {
const Map = dynamic(
() => import('#components/map'), // replace '#components/map' with your component's location
{ ssr: false } // This line is important. It's what prevents server-side render
)
return <Map />
}
export default HomePage
If you want the map to be replaced with something else while it's loading (probably a good practice) you should use the loading property of the dynamic function like this:
import dynamic from 'next/dynamic'
function HomePage() {
const Map = dynamic(
() => import('#components/map'), // replace '#components/map' with your component's location
{
loading: () => <p>A map is loading</p>,
ssr: false // This line is important. It's what prevents server-side render
}
)
return <Map />
}
export default HomePage
Adrian Ciura commented on the flickering which may occur as your components re-render even when nothing about the map should change. They suggest using the new React.useMemo hook to solve that problem. If you do, your code might look something like this:
import React from 'react'
import dynamic from 'next/dynamic'
function HomePage() {
const Map = React.useMemo(() => dynamic(
() => import('#components/map'), // replace '#components/map' with your component's location
{
loading: () => <p>A map is loading</p>,
ssr: false // This line is important. It's what prevents server-side render
}
), [/* list variables which should trigger a re-render here */])
return <Map />
}
export default HomePage
I hope this helps. It would be easier if react-leaflet had a test for the existence of window so it could fail gracefully, but this workaround should work until then.
window is not available in SSR, you probably get this error on your SSR env.
One way to solve this is to mark when the component is loaded in the browser (by using componentDidMount method), and only then render your window required component.
class MyComp extends React.Component {
state = {
inBrowser: false,
};
componentDidMount() {
this.setState({ inBrowser: true });
}
render() {
if (!this.state.inBrowser) {
return null;
}
return <YourRegularComponent />;
}
}
This will work cause componentDidMount lifecycle method is called only in the browser.
Edit - adding the "hook" way
import { useEffect, useState } from 'react';
const MyComp = () => {
const [isBrowser, setIsBrowser] = useState(false);
useEffect(() => {
setIsBrowser(true);
}, []);
if (!isBrowser) {
return null;
}
return <YourRegularComponent />;
};
useEffect hook is an alternative for componentDidMount which runs only inside the browser.
Any component importing from leaflet or react-leaflet should be dynamically imported with option ssr false.
import dynamic from 'next/dynamic';
const MyAwesomeMap = dynamic(() => import('components/MyAwesomeMap'), { ssr: false });
Leaflet considers it outside their scope, as they only bring support on issues happening in vanilla JS environment, so they won't fix (so far)
Create file loader.js, place the code below :
export const canUseDOM = !!(
typeof window !== 'undefined' &&
window.document &&
window.document.createElement
);
if (canUseDOM) {
//example how to load jquery in next.js;
window.$ = window.jQuery = require('jquery');
}
Inside any Component or Page import the file
import {canUseDOM} from "../../utils/loader";
{canUseDOM && <FontAwesomeIcon icon={['fal', 'times']} color={'#4a4a4a'}/>}
or Hook Version
import React, {useEffect, useState} from 'react';
function RenderCompleted() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true)
return () => {
setMounted(false)
}
});
return mounted;
}
export default RenderCompleted;
Invoke Hook:
const isMounted = RenderCompleted();
{isMounted && <FontAwesomeIcon icon={['fal', 'times']} color={'#4a4a4a'}/>}
Tiles are showing incorrectly because you miss leaflet.css. Download it from official website and just add to your project. Suddendly the map tiles will be shown correctly. Well, you won't see anything because default container size is 0,0. To fix it give a size to the container adding this on your styles.css:
.leaflet-container {
height: 450px;
width: 100%;
}
most effective approach is suggested by #FlippingBinary I just need to argue to use little different useMemo functionality. Since useMemo is not consistent to prevent re-render when is used inside function you have to use it separately. This only works form me
vloz.tsx === Page
import React, { Suspense, lazy } from 'react';
const Vloz: React.FC = () => {
const [position, setPosition] = useState<number[] | undefined>();
const AddAd = lazy(() => import('#components/maps/AddAd'));
const MemoMap = React.useMemo(() => {
return (
<Suspense fallback={<p>Loading</p>}>
<AddAd position={position as LatLngTuple} setPosition={setPosition} />
</Suspense>
);
}, [position]);
return (
<div>
{MemoMap}
</div>
);
};
other component is normally used as other tutorials suggest.
This approach will keep Map component render til position change
let divIcon, Map, TileLayer, Marker, Popup;
// first you must check your environment
if (process.browser) {
// then you can import
divIcon = require('leaflet').divIcon;
Map = require('react-leaflet').Map;
TileLayer = require('react-leaflet').TileLayer;
Marker = require('react-leaflet').Marker;
Popup = require('react-leaflet').Popup;
}
In case anyone has a situation that the map component is shared between all pages and you have problem that map component re-renders when route changes (when switching between pages) you must load your map component in _app.js (using techniques explained in the accepted answer) and also consider using shallow routing
We need to use this component without ssr. In this, we will make use of the next js dynamic feature. If you do a set state operation on the map with event handlers, for example, when you click the marker, the map will be rerender. You will need to use react.useMemo for avoid unnecessary re-rendering.
const MapWithNoSSR = React.useMemo(() => dynamic(() => import('../components/Map'), {
loading: () => <p>A map is loading</p>,
ssr: false,
}), []);

Using Google Place Autocomplete API in React

I want to have an auto completing location search bar in my react component, but don't know how I would go about implementing it. The documentation says to include
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places&callback=initMap" async defer></script>
in an HTML file, and then have an initialize function pointing to an element - how would I go about doing this with my react component/JSX? I presume I would have to import the api link, but I have no clue where to go from there.
import React from 'react';
import "https://maps.googleapis.com/maps/api/js?key=MYKEY&libraries=places&callback=initMap";
const SearchBar = () => (
<input type="text" id="search"/> //where I want the google autocomplete to be
);
export default SearchBar;
Google Maps API loading via static import:
import "https://maps.googleapis.com/maps/api/js?key=MYKEY&libraries=places&callback=initMap";
is not supported, you need to consider a different options for that purpose:
reference Google Maps API JS library via /public/index.html file:
<script src="https://maps.googleapis.com/maps/api/js?key=MYKEY&libraries=places"></script>
or dynamically load JS resource, for example using this
library
Now regarding SearchBar component, the below example demonstrates how to implement a simple version of Place Autocomplete (without a dependency to Google Map instance) based on this official example
import React from "react";
/* global google */
class SearchBar extends React.Component {
constructor(props) {
super(props);
this.autocompleteInput = React.createRef();
this.autocomplete = null;
this.handlePlaceChanged = this.handlePlaceChanged.bind(this);
}
componentDidMount() {
this.autocomplete = new google.maps.places.Autocomplete(this.autocompleteInput.current,
{"types": ["geocode"]});
this.autocomplete.addListener('place_changed', this.handlePlaceChanged);
}
handlePlaceChanged(){
const place = this.autocomplete.getPlace();
this.props.onPlaceLoaded(place);
}
render() {
return (
<input ref={this.autocompleteInput} id="autocomplete" placeholder="Enter your address"
type="text"></input>
);
}
}
Here's a solution using ES6 + React Hooks:
First, create a useGoogleMapsApi hook to load the external script:
import { useEffect, useState, useCallback } from 'react'
import loadScript from 'load-script'
import each from 'lodash/each'
var googleMapsApi
var loading = false
var callbacks = []
const useGoogleMapsApi = () => {
const [, setApi] = useState()
const callback = useCallback(() => {
setApi(window.google.maps)
}, [])
useEffect(() => {
if (loading) {
callbacks.push(callback)
} else {
if (!googleMapsApi) {
loading = true
loadScript(
`https://maps.googleapis.com/maps/api/js?key=${process.env.REACT_APP_GOOGLE_MAPS_API_KEY}&libraries=places`,
{ async: true },
() => {
loading = false
googleMapsApi = window.google.maps
setApi(window.google.maps)
each(callbacks, init => init())
callbacks = []
})
}
}
}, [])
return googleMapsApi
}
export default useGoogleMapsApi
Then, here's your input component:
import React, { useRef, useEffect, forwardRef } from 'react'
import useGoogleMapsApi from './useGoogleMapsApi'
const LocationInput = forwardRef((props, ref) => {
const inputRef = useRef()
const autocompleteRef = useRef()
const googleMapsApi = useGoogleMapsApi()
useEffect(() => {
if (googleMapsApi) {
autocompleteRef.current = new googleMapsApi.places.Autocomplete(inputRef.current, { types: ['(cities)'] })
autocompleteRef.current.addListener('place_changed', () => {
const place = autocompleteRef.current.getPlace()
// Do something with the resolved place here (ie store in redux state)
})
}
}, [googleMapsApi])
const handleSubmit = (e) => {
e.preventDefault()
return false
}
return (
<form autoComplete='off' onSubmit={handleSubmit}>
<label htmlFor='location'>Google Maps Location Lookup</label>
<input
name='location'
aria-label='Search locations'
ref={inputRef}
placeholder='placeholder'
autoComplete='off'
/>
</form>
)
}
export default LocationInput
Viola!
Was making a custom address autocomplete for a sign up form and ran into some issues,
// index.html imports the google script via script tag ie: <script src="https://maps.googleapis.com/maps/api/js?key=MYKEY&libraries=places"></script>
import {useState, useRef, useEffect } from 'React'
function AutoCompleteInput(){
const [predictions, setPredictions] = useState([]);
const [input, setInput] = useState('');
const [selectedPlaceDetail, addSelectedPlaceDetail] = useState({})
const predictionsRef = useRef();
useEffect(
()=>{
try {
autocompleteService.current.getPlacePredictions({ input }, predictions => {
setPredictions(predictions);
});
} catch (err) {
// do something
}
}
}, [input])
const handleAutoCompletePlaceSelected = placeId=>{
if (window.google) {
const PlacesService = new window.google.maps.places.PlacesService(predictionsRef.current);
try {
PlacesService.getDetails(
{
placeId,
fields: ['address_components'],
},
place => addSelectedPlaceDetail(place)
);
} catch (e) {
console.error(e);
}
}
}
return (
<>
<input onChange={(e)=>setInput(e.currentTarget.value)}
<div ref={predictionsRef}
{ predictions.map(prediction => <div onClick={ ()=>handleAutoCompletePlaceSelected(suggestion.place_id)}> prediction.description </div> )
}
</div>
<>
)
}
So basically, you setup the autocomplete call, and get back the predictions results in your local state.
from there, map and show the results with a click handler that will do the follow up request to the places services with access to the getDetails method for the full address object or whatever fields you want.
you then save that response to your local state and off you go.

Categories

Resources