setState doesn't re-render react functional component - reactjs

I am getting user's location and setting it as the new state at "onSuccess" function,
the component doesn't re-render.
After checking a lot i have seen that react doesn't see it as a change of state because it is an array in that case and it doesn't pass react's "equality" check as a state that was changed.
With that, nothing that i have tried has worked. any ideas?
import { useEffect, useState } from "react";
import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
export default function Map() {
const [location, setLocation] = useState([52.234, 13.413]);
const onSuccess = (position) => {
let userLocation = [position.coords.latitude, position.coords.longitude];
setLocation([...userLocation]);
};
useEffect(() => {
if (!("geolocation" in navigator)) {
alert("no Geolocation available");
}
navigator.geolocation.getCurrentPosition(onSuccess);
}, []);
console.log(location);
return (
<>
<MapContainer
className="leaflet-map"
center={location}
zoom={11}
scrollWheelZoom={false}
>
<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>
</>
);
}

It looks like MapContainer does not recenter after mounting even if the center prop is changed as mentioned in the docs:
https://react-leaflet.js.org/docs/api-map
Except for its children, MapContainer props are immutable: changing them after they have been set a first time will have no effect on the Map instance or its container.
You could force replacing the MapContainer by passing a key prop that you change whenever location changes, for example:
<MapContainer
key={`${location[0]}-${location[1]}`}
Or investigating other options in react-leaflet such as useMap to get access to the Leaflet Map instance and calling map.setView(...) https://leafletjs.com/reference-1.7.1.html#map-setview

Are you able to confirm that onSuccess is called at all? It may be that getCurrentPosition is running into an error, so calling it with two arguments would be good:
navigator.geolocation.getCurrentPosition(onSuccess, onError);
You should also include onSuccess in the useEffect dependencies.
useEffect(() => {
if (!("geolocation" in navigator)) {
alert("no Geolocation available");
}
navigator.geolocation.getCurrentPosition(onSuccess);
}, [onSuccess]);
And to prevent multiple calls to getCurrentPosition due to onSuccess changing, you should also useCallback with the dependency on setLocation:
const onSuccess = useCallback((position) => {
let userLocation = [position.coords.latitude, position.coords.longitude];
setLocation([...userLocation]);
}, [setLocation]);

Related

Why is my React Native component not re-rendering on state update?

I'm struggling with this React-Native component for a few days now. You should probably know that React-Native is kind of new to me so... sorry if the solution is obvious to you.
I'm using react-native-maps and I have several markers on my map. Each one of them has some data stored in my state and I want the callout to display a piece of this state on press.
Here are my states :
const [markersDetails, setMarkersDetails] = useState([]);
const [activeMarker, setActiveMarker] = useState({});
My activeMarker is updated by this function :
const markerSearch = (markerId) => {
let stockMarker = markersDetails.find((singleMarker) => {
return Number(singleMarker.idMarker) === markerId;
});
console.log("Stock ", stockMarker);
setActiveMarker(stockMarker);
console.log("State ", activeMarker);
};
And this function is called, inside my return, with the onPress of any marker :
<Marker
key={Number(marker.index)}
coordinate={{
latitude: Number(marker.latitude),
longitude: Number(marker.longitude),
}}
pinColor="blue"
onPress={() => {
markerSearch(Number(marker.index));
}}
>
{activeMarker !== {} && activeMarker.markerName && (
<Callout>
<View>
<Text>{activeMarker.markerName}</Text>
</View>
</Callout>
)}
</Marker>
But whenever I press on a marker, the callout opens immediatly while my state is not yet updated. So the text within the callout refers either to the previous marker or is empty (if it's the first marker I press on).
I've checked with console.log and my state is clearly updated but it takes a little bit more time. And I don't know why my callout is not re-rendering when this state is updating.
I've tried a ton of things to make this works but I can't figure this out...
Try doing something like that:
You can extract the section to a new component
Then inside this use the useEffect hook
export default function CalloutComponent({activeMarker}) {
const [markerName, setMarkerName] = useState('')
useEffect(() => {
setMarkerName(activeMarker?.markerName)
}, [activeMarker?.markerName])
if(!!markerName) return null
return (
<Callout>
<View>
<Text>{markerName}</Text>
</View>
</Callout>
)
}
And use this new component in your Main view
<Marker
...
>
<CalloutComponent activeMarker={activeMarker}/>
</Marker>

Custom page / route transitions in Next.js

I'm trying to achieve callback-based route transitions using Next.js's framework and Greensock animation library (if applicable). For example when I start on the homepage and then navigate to /about, I want to be able to do something like:
HomepageComponent.transitionOut(() => router.push('/about'))
ideally by listening to the router like a sort of middleware or something before pushing state
Router.events.on('push', (newUrl) => { currentPage.transitionOut().then(() => router.push(newUrl)) });
Main Problem
The main problem is that I also have a WebGL app running in the background, decoupled from the React ecosystem (since it uses requestAnimationFrame). So the reason I want callback-based transitions is because I need to run them after the WebGL transitions are done.
Current Implementation
I've looked into using React Transition Group and I've seen the docs for the Router object but neither seems to be callback-based. In other words, when I transition to a new page, the WebGL and the page transitions run at the same time. And I don't want to do a hacky solution like adding a delay for the page transitions so they happen after the WebGL ones.
This is what I have right now:
app.js
<TransitionGroup>
<Transition
timeout={{ enter: 2000, exit: 2000 }}
// unmountOnExit={true}
onEnter={(node) => {
gsap.fromTo(node, { opacity: 0 }, { opacity: 1, duration: 1 });
}}
onExit={(node) => {
gsap.to(node, { opacity: 0, duration: 1 });
}}
key={router.route}
>
<Component {...pageProps}></Component>
</Transition>
</TransitionGroup>
webgl portion
Router.events.on('routeChangeStart', (url) => {
// transition webGL elements
// ideally would transition webGL elements and then allow callback to transition out html elements
});
I've also tried using the eventemitter3 library to do something like:
// a tag element click handler
onClick(e, href) {
e.preventDefault();
this.transitionOut().then(() => { Emitter.emit('push', href); });
// then we listen to Emitter 'push' event and that's when we Router.push(href)
}
However this method ran into huge issues when using the back / forward buttons for navigating
Bit late on this but I was looking into this myself today. It's really easy to use Framer Motion for this but I also wanted to use GSAP / React Transition Group.
For Framer Motion I just wrapped the Next < Component > with a motion component:
<motion.div
key={router.asPath}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<Component {...pageProps} />
</motion.div>
For GSAP / React Transition Group, not sure if this is the right way but it's working as intended for me (see comments):
const [state, setstate] = useState(router.asPath) // I set the current asPath as the state
useEffect(() => {
const handleStart = () => {
setstate(router.asPath) // then on a router change, I'm setting the state again
// other handleStart logic goes here
}
const handleStop = () => {
... // handleStop logic goes here
}
router.events.on("routeChangeStart", handleStart)
router.events.on("routeChangeComplete", handleStop)
router.events.on("routeChangeError", handleStop)
return () => {
router.events.off("routeChangeStart", handleStart)
router.events.off("routeChangeComplete", handleStop)
router.events.off("routeChangeError", handleStop)
}
}, [router])
<Transition
in={router.asPath !== state} // here I'm just checking if the state has changed, then triggering the animations
onEnter={enter => gsap.set(enter, { opacity: 0 })}
onEntered={entered => gsap.to(entered, { opacity: 1, duration: 0.3 })}
onExit={exit => gsap.to(exit, { opacity: 0, duration: 0.3 })}
timeout={300}
appear
>
<Component {...pageProps} />
</Transition>
First I recommend reading Greensock’s React documentation.
Intro Animations in Next.JS
For intro animations, if you use useLayoutEffect with SSR your console will fill up with warnings. To avoid this apply useIsomorphicLayoutEffect instead. Go to useIsomorphicLayoutEffect.
To prevent the flash of unstyled content (FOUC) with SSR, you need to set the initial styling state of the component. For example, if we are fading in, the initial style of that component should be an opacity of zero.
Outro Animations in Next.JS
For outro animations, intercept the page transition, and do the exit animations, then onComplete route to the next page.
To pull this off, we can use TransitionLayout higher order component as a wrapper to delay the routing change until after any animations have completed, and a TransitionProvider component that will take advantage of React’s useContext hook to share an outro timeline across multiple components, regardless of where they are nested.
Transition Context
In order to make a page transition effect, we need to prevent rendering the new page before our outro animation is done.
We may have many components with different animation effects nested in our pages. To keep track of all the different outro transitions, we will use a combination of React’s Context API and a top-level GSAP timeline.
In TransitionContext we will create our TransitionProvider which will make our GSAP timeline for outro animations available to any components who would like to transition out during a page change.
import React, { useState, createContext, useCallback } from "react"
import gsap from "gsap"
const TransitionContext = createContext({})
const TransitionProvider = ({ children }) => {
const [timeline, setTimeline] = useState(() =>
gsap.timeline({ paused: true })
)
return (
<TransitionContext.Provider
value={{
timeline,
setTimeline,
}}
>
{children}
</TransitionContext.Provider>
)
}
export { TransitionContext, TransitionProvider }
Next, we have TransitionLayout which will be our controller that will initiate the outro animations and update the page when they are all complete.
import { gsap } from "gsap"
import { TransitionContext } from "../context/TransitionContext"
import { useState, useContext, useRef } from "react"
import useIsomorphicLayoutEffect from "../animation/useIsomorphicLayoutEffect"
export default function TransitionLayout({ children }) {
const [displayChildren, setDisplayChildren] = useState(children)
const { timeline, background } = useContext(TransitionContext)
const el = useRef()
useIsomorphicLayoutEffect(() => {
if (children !== displayChildren) {
if (timeline.duration() === 0) {
// there are no outro animations, so immediately transition
setDisplayChildren(children)
} else {
timeline.play().then(() => {
// outro complete so reset to an empty paused timeline
timeline.seek(0).pause().clear()
setDisplayChildren(children)
})
}
}
}, [children])
return <div ref={el}>{displayChildren}</div>
}
In a custom App component, we can have TransitionProvider and TransitionLayout wrap the other elements so they can access the TransitionContext properties. Header and Footer exist outside of Component so that they will be static after the initial page load.
import { TransitionProvider } from "../src/context/TransitionContext"
import TransitionLayout from "../src/animation/TransitionLayout"
import { Box } from "theme-ui"
import Header from "../src/ui/Header"
import Footer from "../src/ui/Footer"
export default function MyApp({ Component, pageProps }) {
return (
<TransitionProvider>
<TransitionLayout>
<Box
sx={{
display: "flex",
minHeight: "100vh",
flexDirection: "column",
}}
>
<Header />
<Component {...pageProps} />
<Footer />
</Box>
</TransitionLayout>
</TransitionProvider>
)
}
Component-Level Animation
Here is an example of a basic animation we can do at the component level. We can add as many of these as we want to a page and they will all do the same thing, wrap all its children in a transparent div and fade it in on page load, then fade out when navigating to a different page.
import { useRef, useContext } from "react"
import { gsap } from "gsap"
import { Box } from "theme-ui"
import useIsomorphicLayoutEffect from "./useIsomorphicLayoutEffect"
import { TransitionContext } from "../context/TransitionContext"
const FadeInOut = ({ children }) => (
const { timeline } = useContext(TransitionContext)
const el = useRef()
// useIsomorphicLayoutEffect to avoid console warnings
useIsomorphicLayoutEffect(() => {
// intro animation will play immediately
gsap.to(el.current, {
opacity: 1,
duration: 1,
})
// add outro animation to top-level outro animation timeline
timeline.add(
gsap.to(el.current, {
opacity: 1,
duration: .5,
}),
0
)
}, [])
// set initial opacity to 0 to avoid FOUC for SSR
<Box ref={el} sx={{opacity: 0}}>
{children}
</Box>
)
export default FadeInOut
We can take this pattern and extract it into an extendable AnimateInOut helper component for reusable intro/outro animation patterns in our app.
import React, { useRef, useContext } from "react"
import { gsap } from "gsap"
import { Box } from "theme-ui"
import useIsomorphicLayoutEffect from "./useIsomorphicLayoutEffect"
import { TransitionContext } from "../context/TransitionContext"
const AnimateInOut = ({
children,
as,
from,
to,
durationIn,
durationOut,
delay,
delayOut,
set,
skipOutro,
}) => {
const { timeline } = useContext(TransitionContext)
const el = useRef()
useIsomorphicLayoutEffect(() => {
// intro animation
if (set) {
gsap.set(el.current, { ...set })
}
gsap.to(el.current, {
...to,
delay: delay || 0,
duration: durationIn,
})
// outro animation
if (!skipOutro) {
timeline.add(
gsap.to(el.current, {
...from,
delay: delayOut || 0,
duration: durationOut,
}),
0
)
}
}, [])
return (
<Box as={as} sx={from} ref={el}>
{children}
</Box>
)
}
export default AnimateInOut
The AnimateInOut component has built in flexibility for different scenarios:
Setting different animations, durations and delays for intros and outros
Skipping the outro
Setting the element tag for the wrapper, e.g. use a <span> instead of a <div>
Use GSAP’s set option to define initial values for the intro
Using this we can create all sorts of reusable intro/outro animations, such as <FlyInOut>, <ScaleInOut>, <RotateInOut3D> and so forth.
I have a demo project where you can see the above in practice: TweenPages

Trigger re-render of subcomponent (react-table) using hooks

I'm still new to React, and functional programming, and Javascript, and JSX, so go easy if this is a stupid question.
I'm modifying one of the example material-ui tables from react-table v7. The original code can be found here. The example is completely functional and is using React Hooks as opposed to classes, as do all of the components of the template I'm using (shout out to creative-tim.com!)
My parent function (representative of a page in my dashboard application), for instance Users.js or Stations.js fetches data from a backend api inside a useEffect hook. That data is then passed as a prop to my subcomponent ReactTables.js
For some reason ReactTables.js does not receive changes to the "data" prop after the parent page's useEffect finishes. However, once I modify the data from a subcomponent of ReactTables (in this case AddAlarmDialog.js) then the table re-renders and all of my data suddenly appears.
How can I trigger the re-render of my subcomponent when data is returned from the parent component's useEffect? I noticed that in older versions of React there was a lifecycle function called componentWillReceiveProps(). Is this the behavior I need to emulate here?
Example Parent Component (Alarms.js):
import React, { useEffect, useState } from "react";
// #material-ui/core components
// components and whatnot
import GridContainer from "components/Grid/GridContainer.js";
import GridItem from "components/Grid/GridItem.js";
import ReactTables from "../../components/Table/ReactTables";
import { server } from "../../variables/sitevars.js";
export default function Alarms() {
const [columns] = useState([
{
Header: "Alarm Name",
accessor: "aName"
},
{
Header: "Location",
accessor: "aLocation"
},
{
Header: "Time",
accessor: "aTime"
},
{
Header: "Acknowledged",
accessor: "aAcked"
},
{
Header: "Active",
accessor: "aActive"
}
]);
const [data, setData] = useState([]);
const [tableType] = useState("");
const [tableLabel] = useState("Alarms");
useEffect(() => {
async function fetchData() {
const url = `${server}/admin/alarms/data`;
const response = await fetch(url);
var parsedJSON = JSON.parse(await response.json());
var tableElement = [];
parsedJSON.events.forEach(function(alarm) {
tableElement = [];
parsedJSON.tags.forEach(function(tag) {
if (alarm.TagID === tag.IDX) {
tableElement.aName = tag.Name;
}
});
tableElement.aTime = alarm.AlarmRcvdTime;
parsedJSON.sites.forEach(function(site) {
if (site.IDX === alarm.SiteID) {
tableElement.aLocation = site.Name;
}
});
if (alarm.Active) {
tableElement.aActive = true;
} else {
tableElement.aActive = false;
}
if (!alarm.AckedBy && !alarm.AckedTime) {
tableElement.aAcked = false;
} else {
tableElement.aAcked = true;
}
//const newData = data.concat([tableElement]);
//setData(newData);
data.push(tableElement);
});
}
fetchData().then(function() {
setData(data);
});
}, [data]);
return (
<div>
<GridContainer>
<GridItem xs={12} sm={12} md={12} lg={12}>
<ReactTables
data={data}
columns={columns}
tableType={tableType}
tableLabel={tableLabel}
></ReactTables>
</GridItem>
</GridContainer>
</div>
);
}
Universal Table Subcomponent (ReactTables.js):
import React, { useState } from "react";
// #material-ui/core components
import { makeStyles } from "#material-ui/core/styles";
// #material-ui/icons
import Assignment from "#material-ui/icons/Assignment";
// core components
import GridContainer from "components/Grid/GridContainer.js";
import GridItem from "components/Grid/GridItem.js";
import Card from "components/Card/Card.js";
import CardBody from "components/Card/CardBody.js";
import CardIcon from "components/Card/CardIcon.js";
import CardHeader from "components/Card/CardHeader.js";
import { cardTitle } from "assets/jss/material-dashboard-pro-react.js";
import PropTypes from "prop-types";
import EnhancedTable from "./subcomponents/EnhancedTable";
const styles = {
cardIconTitle: {
...cardTitle,
marginTop: "15px",
marginBottom: "0px"
}
};
const useStyles = makeStyles(styles);
export default function ReactTables(props) {
const [data, setData] = useState(props.data);
const [columns] = useState(props.columns);
const [tableType] = useState(props.tableType);
const [skipPageReset, setSkipPageReset] = useState(false)
const updateMyData = (rowIndex, columnId, value) => {
// We also turn on the flag to not reset the page
setData(old =>
old.map((row, index) => {
if (index === rowIndex) {
return {
...old[rowIndex],
[columnId]: value
};
}
return row;
})
);
};
const classes = useStyles();
return (
<GridContainer>
<GridItem xs={12}>
<Card>
<CardHeader color="primary" icon>
<CardIcon color="primary">
<Assignment />
</CardIcon>
<h4 className={classes.cardIconTitle}>{props.tableLabel}</h4>
</CardHeader>
<CardBody>
<EnhancedTable
data={data}
columns={columns}
tableType={tableType}
setData={setData}
updateMyData={updateMyData}
skipPageReset={skipPageReset}
filterable
defaultPageSize={10}
showPaginationTop
useGlobalFilter
showPaginationBottom={false}
className="-striped -highlight"
/>
</CardBody>
</Card>
</GridItem>
</GridContainer>
);
}
ReactTables.propTypes = {
columns: PropTypes.array.isRequired,
data: PropTypes.array.isRequired,
tableType: PropTypes.string.isRequired,
tableLabel: PropTypes.string.isRequired,
updateMyData: PropTypes.func,
setData: PropTypes.func,
skipPageReset: PropTypes.bool
};
**For the record: if you notice superfluous code in the useEffect it's because I was messing around and trying to see if I could trigger a re-render.
I dont know exactly how the reactTable is handling its rendering, but if its a pure functional component, then the props you pass to it need to change before it will re-evaluate them. When checking if props have changed, react will just do a simple === comparison, which means that if your props are objects whos properties are being modified, then it will still evaluate as the same object. To solve this, you need to treat all props as immutable
In your example, you are pushing to the data array, and then calling setData(data) which means that you are passing the same instance of the array. When react compares the previous version of data, to the new version that you are setting in the call to setDate, it will think data hasnt changed because it is the same reference.
To solve this, you can just make a new array from the old array by spreading the existing array into a new one. So, instead of doing
data.push(tableElement);
You should do
const newInstance = [...data, tableElement];
Your code will need some tweaking because it looks like you are adding in lots of tableElements. But the short version of the lesson here is that you should never try and mutate your props. Always make a new instance
EDIT: So, after looking again, I think the problem is the way you are using the default param in the useState hook. It looks like you are expecting that to set the state from any prop changes, but in reality, that param is simply the default value that you will put in the component when it is first created. Changing the incoming data prop doesn't alter your state in any way.
If you want to update state in response to changes in props, you will need to use the useEffect hook, and set the prop in question as a dependancy.
But personally, I would try and not have what is essentially the same data duplicated in state in two places. I think the best bet would be to store your data in your alarm component, and add a dataChanged callback or something which will take your new data prop, and pass it back up to alarm via a parameter in the callback

React functional component using the `useState` hook does not update onChange using ReactiveSearch

I am trying to use the ReactiveSearch component library to build a basic search application, and need to use the components as controlled component (https://reactjs.org/docs/forms.html). For all of the other filters I am working with, this is no problem, and the app detects changes and updates accordingly. However, for this DateRange component, it won't work. My working hypothesis is that it has something to do with the state value being an object rather than an array, but I can't find evidence to support that yet.
I've also tried using a regular class component, with the same result.
Link to Sandbox: https://codesandbox.io/s/ecstatic-ride-bly6r?fontsize=14&hidenavigation=1&theme=dark
Basic code snippet with no other filters
import React, { useState } from "react";
import {
ReactiveBase,
ResultsList,
DateRange,
SelectedFilters
} from "#appbaseio/reactivesearch";
const App = props => {
const [filterState, setFilterState] = useState({
DateFilter: { start: new Date(), end: new Date() }
});
return (
<div className="App">
<ReactiveBase
app="good-books-ds"
credentials="nY6NNTZZ6:27b76b9f-18ea-456c-bc5e-3a5263ebc63d"
>
<DateRange
value={filterState.DateFilter}
onChange={value => {
setFilterState({
...filterState,
DateFilter: {
start: value.start,
end: value.end
}
});
}}
componentId="DateFilter"
dataField="timestamp"
/>
<SelectedFilters />
</ReactiveBase>
</div>
);
};
export default App;
Just changing value to defaultValue worked for me (https://codesandbox.io/s/jolly-spence-1o8bv).
<DateRange
defaultValue={filterState.DateFilter}
onChange={value => {
setFilterState({
DateFilter: {
start: value.start,
end: value.end
}
});
}}
componentId="DateFilter"
dataField="timestamp"
/>
I also removed the DateFilter spread in your setFilterState, since your previous state was being fully overwritten regardless.
It turned out to be an underlying problem with how the ReactiveSearch library was comparing the dates, as well as not setting values properly. Will make a PR to fix it.

useState not setting after initial setting

I have a functional component that is using useState and uses the #react-google-maps/api component. I have a map that uses an onLoad function to initalize a custom control on the map (with a click event). I then set state within this click event. It works the first time, but every time after that doesn't toggle the value.
Function component:
import React, { useCallback } from 'react';
import { GoogleMap, LoadScript } from '#react-google-maps/api';
export default function MyMap(props) {
const [radiusDrawMode, setRadiusDrawMode] = React.useState(false);
const toggleRadiusDrawMode = useCallback((map) => {
map.setOptions({ draggableCursor: (!radiusDrawMode) ? 'crosshair' : 'grab' });
setRadiusDrawMode(!radiusDrawMode);
}, [setRadiusDrawMode, radiusDrawMode]); // Tried different dependencies.. nothing worked
const mapInit = useCallback((map) => {
var radiusDiv = document.createElement('div');
radiusDiv.index = 1;
var radiusButton = document.createElement('div');
radiusDiv.appendChild(radiusButton);
var radiusText = document.createElement('div');
radiusText.innerHTML = 'Radius';
radiusButton.appendChild(radiusText);
radiusButton.addEventListener('click', () => toggleRadiusDrawMode(map));
map.controls[window.google.maps.ControlPosition.RIGHT_TOP].push(radiusDiv);
}, [toggleRadiusDrawMode, radiusDrawMode, setRadiusDrawMode]); // Tried different dependencies.. nothing worked
return (
<LoadScript id="script-loader" googleMapsApiKey="GOOGLE_API_KEY">
<div className="google-map">
<GoogleMap id='google-map'
onLoad={(map) => mapInit(map)}>
</GoogleMap>
</div>
</LoadScript>
);
}
The first time the user presses the button on the map, it setss the radiusDrawMode to true and sets the correct cursor for the map (crosshair). Every click of the button after does not update radiusDrawMode and it stays in the true state.
I appreciate any help.
My guess is that it's a cache issue with useCallback. Try removing the useCallbacks to test without that optimization. If it works, you'll know for sure, and then you can double check what should be memoized and what maybe should not be.
I'd start by removing the one from toggleRadiusDrawMode:
const toggleRadiusDrawMode = map => {
map.setOptions({ draggableCursor: (!radiusDrawMode) ? 'crosshair' : 'grab' });
setRadiusDrawMode(!radiusDrawMode);
};
Also, can you access the state of the map options (the ones that you're setting with map.setOptions)? If so, it might be worth using the actual state of the map's option rather than creating your own internal state to track the same thing. Something like (I'm not positive that it would be map.options):
const toggleRadiusDrawMode = map => {
const { draggableCursor } = map.options;
map.setOptions({
draggableCursor: draggableCursor === 'grab' ? 'crosshair' : 'grab'
});
};
Also, I doubt this is the issue, but it looks like you're missing a closing bracket on the <GoogleMap> element? (Also, you might not need to create the intermediary function between onLoad and mapInit, and can probably pass mapInit directly to the onLoad.)
<GoogleMap id='google-map'
onLoad={mapInit}>
This is the solution I ended up using to solve this problem.
I basically had to switch out using a useState(false) for setRef(false). Then set up a useEffect to listen to changes on the ref, and in the actual toggleRadiusDraw I set the reference value which fires the useEffect to set the actual ref value.
import React, { useCallback, useRef } from 'react';
import { GoogleMap, LoadScript } from '#react-google-maps/api';
export default function MyMap(props) {
const radiusDrawMode = useRef(false);
let currentRadiusDrawMode = radiusDrawMode.current;
useEffect(() => {
radiusDrawMode.current = !radiusDrawMode;
});
const toggleRadiusDrawMode = (map) => {
map.setOptions({ draggableCursor: (!currentRadiusDrawMode) ? 'crosshair' : 'grab' });
currentRadiusDrawMode = !currentRadiusDrawMode;
};
const mapInit = (map) => {
var radiusDiv = document.createElement('div');
radiusDiv.index = 1;
var radiusButton = document.createElement('div');
radiusDiv.appendChild(radiusButton);
var radiusText = document.createElement('div');
radiusText.innerHTML = 'Radius';
radiusButton.appendChild(radiusText);
radiusButton.addEventListener('click', () => toggleRadiusDrawMode(map));
map.controls[window.google.maps.ControlPosition.RIGHT_TOP].push(radiusDiv);
});
return (
<LoadScript id="script-loader" googleMapsApiKey="GOOGLE_API_KEY">
<div className="google-map">
<GoogleMap id='google-map'
onLoad={(map) => mapInit(map)}>
</GoogleMap>
</div>
</LoadScript>
);
}
Not sure if this is the best way to handle this, but hope it helps someone else in the future.

Resources