Clearing map layers with react-leaflet and hooks - reactjs

I'm building a custom plugin for react-leaflet to locate the user using leaflet's locate method.
It works basically, but with the one problem of the layer not clearing between turning location off and back on again. Each time the locate button is toggled, locate should start fresh.
Here is a codesandbox of the problem. As you toggle the button, the circle becomes darker as the layers are stacked on top of each other.
Here is the component:
import React, { useEffect, useState, useRef } from 'react'
import L from 'leaflet'
import { useLeaflet } from 'react-leaflet'
import LocationSearchingIcon from '#material-ui/icons/LocationSearching';
import MapButton from './MapButton'
function Locate() {
const { map } = useLeaflet();
const [locate, setLocate] = useState(false);
function toggleLocate() {
setLocate(!locate)
}
console.log(locate)
const layerRef = useRef(L.layerGroup());
useEffect(() => {
if (locate) {
map.removeLayer(layerRef.current)
map.locate({ setView: false, watch: true, enableHighAccuracy: true }) /* This will return map so you can do chaining */
.on('locationfound', function (e) {
L.circleMarker([e.latitude, e.longitude], {
radius: 10,
weight: 3,
color: 'white',
fillColor: 'blue',
fillOpacity: 1
}).addTo(layerRef.current);
L.circle([e.latitude, e.longitude], e.accuracy / 2, {
weight: 1,
color: 'blue',
fillColor: 'blue',
fillOpacity: 0.2
}).addTo(layerRef.current);
window.localStorage.setItem('accuracy', e.accuracy)
map.setView([e.latitude, e.longitude], 16)
layerRef.current.addTo(map)
})
.on('locationerror', function (e) {
alert("Location error or access denied.");
})
} if (!locate) {
map.stopLocate();
map.removeLayer(layerRef.current);
}
}, [locate, map]);
return (
<MapButton
title={locate ? 'Click to disable location' : 'Click to locate'}
onClick={toggleLocate}
left
top={102}
>
<LocationSearchingIcon fontSize="small" style={{ color: locate ? 'orange' : '' }} />
</MapButton>
)
}
export default Locate
I would appreciate any solution or tips to stop the layers stacking, and clear properly went the button is toggled. Thanks

As Falke Design's mentions in a comment above:
Add the circle to a featureGroup and then every time locationfound is fired, you call featuregroup.clearLayers() and then add the circle new to the featureGroup
This solved the problem.
The working codesandbox is here

Related

GoogleMaps api : why does my map state become null?

Help appreciated, I'm stuck !
What I try to do
I display a Google map with a set of marker.
When I click on a marker, I want to add a Google circle to the map.
What happens
When I click on a first marker, no circle is displayed.
But when I click on a second marker and more, they are displayed !
Why it does not work
I've tracked the map state value with console.log.
The problem is that when I first go to the MarkkerClicked function, for an unknown reason, the "map" state's value is "null" ! So no circle is created.
And, even stranger, the map state contains a map instance when the map is first loaded, and also when I click a second marker.
Can you tell me what I have done wrong, that makes the map value set to null when the first marker is clicked ?
My component :
import { GoogleMap, MarkerF, useJsApiLoader } from "#react-google-maps/api";
import Box from '#mui/material/Box';
import { useSelector } from 'react-redux'
let mapCircle1 = null
export default function MapPage() {
// The array of markers is in the REDUX store
const selectMarkersArray = state => state.markersArray
const markersArray = useSelector(selectMarkersArray)
// This state contains the selected marker (or null if no marker selected)
const [selectedMarker, setSelectedMarker] = useState(null);
// Options for GoogleMaps
let center = {
lat: 43.3318,
lng: 5.0550
}
let zoom = 15
const containerStyle = {
width: "100%",
height: "100%"
}
// GoogleMaps loading instructions
const { isLoaded } = useJsApiLoader({
id: 'google-map-script',
googleMapsApiKey: "MY-GOOGLE-KEY"
})
const [map, setMap] = useState(null)
const onLoad = useCallback(function callback(map) {
setMap(map)
console.log('map value in onLoad :')
console.log(map)
}, [])
const onUnmount = useCallback(function callback(map) {
setMap(null)
}, [])
// Function executed when a marker is clicked
function markerClicked(props) {
console.log('map value in markerClicked :')
console.log(map)
// I create a new Circle data
let circleOption1 = {
fillColor: "#2b32ac ",
map: map,
center: {lat:props.marker.spotLatitude, lng:props.marker.spotLongitude},
radius: props.marker.spotCircleRadius,
};
mapCircle1 = new window.google.maps.Circle(circleOption1);
// I update the selecte marker state
setSelectedMarker({...props.marker})
}
return (isLoaded ? (
<Box height="80vh" display="flex" flexDirection="column">
<GoogleMap
mapContainerStyle={containerStyle}
center={center}
zoom={zoom}
onLoad={onLoad}
onUnmount={onUnmount}
>
{markersArray.map((marker, index) => {
return (
<MarkerF
key={index.toString()}
position={{lat:marker.spotLatitude, lng:marker.spotLongitude}}
onClick={() => markerClicked({marker:marker})}
>
</MarkerF>
)
})}
</GoogleMap>
</Box>
) : <></>
)
};
And the console.log (first log when the map is loaded, second when the first marker is clicked, third when another marker is clicked):
map value in onLoad :
jj {gm_bindings_: {…}, __gm: hda, gm_accessors_: {…}, mapCapabilities: {…}, renderingType: 'UNINITIALIZED', …}
map value in markerClicked :
null
map value in markerClicked :
jj {gm_bindings_: {…}, __gm: hda, gm_accessors_: {…}, mapCapabilities: {…}, renderingType: 'RASTER', …}```
It coulds be because you are not using State Hooks and the <Circle /> component for rendering / updating circles
I was able to reproduce your code on codesandbox and confirmed that the map is indeed returning null per marker click. I'm still unsure as to why it really is happening, but I managed to fix it and managed to render a circle even on the first marker click, after modifying your code by utilizing State Hooks for your circles, and also using the <Circle /> component as per the react-google-maps/api library docs.
I just managed to hardcode some stuff since I did not use redux to reproduce your code but one of the things I did is create a circles array State Hook:
// this array gets updated whenever you click on a marker
const [circles, setCircles] = useState([
{
id: "",
center: null,
circleOptions: {
strokeColor: "#FF0000",
strokeOpacity: 0.8,
strokeWeight: 2,
fillColor: "#FF0000",
fillOpacity: 0.35,
clickable: false,
draggable: false,
editable: false,
visible: true,
radius: 100,
zIndex: 1
}
}
]);
Just make sure to add the useState on your import.
Then for demonstration, I hardcoded a markers array:
// This could be your array of marker using redux,
// I just hardcoded this for demonstration
const markersArray = [
{
id: 1,
position: { lat: 43.333194, lng: 5.050184 }
},
{
id: 2,
position: { lat: 43.336356, lng: 5.053353 }
},
{
id: 3,
position: { lat: 43.331609, lng: 5.056403 }
},
{
id: 4,
position: { lat: 43.328806, lng: 5.058998 }
}
];
Then here's my markerClicked function looks like:
// Function executed when a marker is clicked
const markerClicked = (marker) => {
console.log("map value on marker click: ");
console.log(map);
// This stores the marker coordinates
// in which we will use for the center of your circle
const markerLatLng = marker.latLng.toJSON();
// this will update our circle array
// adding another object with different center
// the center is fetched from the marker that was clicked
setCircles((prevState) => {
return [
...prevState,
{
id: "",
center: markerLatLng,
circleOptions: {
strokeColor: "#FF0000",
strokeOpacity: 0.8,
strokeWeight: 2,
fillColor: "#FF0000",
fillOpacity: 0.35,
clickable: false,
draggable: false,
editable: false,
visible: true,
radius: 100,
zIndex: 1
}
}
];
});
// this is for you to see that the circles array is updated.
console.log(circles);
};
Please do note that the onClick attribute for <MarkerF /> automatically returns a parameter that includes the marker's coordinates and other stuff, you can just try to use console.log if you wanna check. Also make sure that the value you put on the onClick is equals to the name of the function alone. In this case, it is markerClicked, you'll see later on.
Note: I also added a mapClicked function for you to be able to clear the circles array.
Please see proof of concept sandbox below.
Then this is how I rendered the <GoogleMap /> component with <MarkerF /> and <Circle /> components as its children.
<GoogleMap
mapContainerStyle={containerStyle}
center={center}
zoom={zoom}
onLoad={onLoad}
onUnmount={onUnmount}
onClick={mapClicked}
>
{markersArray.map((marker, index) => {
return (
<MarkerF
key={index.toString()}
position={{ lat: marker.position.lat, lng: marker.position.lng }}
onClick={markerClicked}
></MarkerF>
);
})}
{/* This maps through the circles array, so when the array gets updated,
another circle is added */}
{circles.map((circle, index) => {
return (
<Circle
key={index.toString()}
// required
center={circle.center}
// required
options={circle.circleOptions}
/>
);
})}
</GoogleMap>
With all these, the map value on marker click does not return null, and a circle gets rendered even on first marker click.
Here's a proof of concept sandbox for you to check and see how it works ( Make sure to use your own API key to test ): https://codesandbox.io/s/proof-of-concept-show-circle-on-marker-click-s175wl?file=/src/Map.js
Note: There's some weird offset between the marker and the circle if you zoom out too far, but seems fine when you zoom in. I have encountered this on some previous questions here in SO when rendering polylines and I don't know why is that or how to fix it.
With that said, hope this helps!

OpenLayers6 and React

I am trying to implement OpenLayers6 ("ol": "^6.14.1") into a ReactJS project, but all the documentation is created in normal JS files and I can't find any good examples or tutorials with functional components and OpenLayers6.
I have troubles figuring out how to implement the majority of things, because the docs seem to ignore the lifecycle of React.
What I've managed to do until now is to add a marker and a popup right above the marker; to close the popup and delete the marker by deleting the whole vector layer(which seems overkill).
import { useState, useEffect, useRef } from 'react';
// import ModalUI from '../UI/ModalUI';
import classes from './MapUI.module.css';
import { drawerActions } from '../../store/drawer-slice';
import 'ol/ol.css';
import { Map, View, Overlay, Feature } from 'ol';
import Point from 'ol/geom/Point';
import { Vector as VectorLayer } from 'ol/layer';
import VectorSource from 'ol/source/Vector';
import { fromLonLat, toLonLat } from 'ol/proj';
import { toStringHDMS } from 'ol/coordinate';
import TileLayer from 'ol/layer/Tile';
import OSM from 'ol/source/OSM';
import PopUp from './PopUp';
import { useDispatch } from 'react-redux';
export default function MapUI() {
const mapRef = useRef();
const popup = useRef();
const [coordinates, setCoordinates] = useState('');
const [newMarker, setNewMarker] = useState(
new Feature({
geometry: new Point([[]]),
name: '',
})
);
const [newMarkersLayer, setNewMarkersLayer] = useState(
new VectorLayer({
properties: { name: 'newMarkers' },
source: new VectorSource({
features: [newMarker],
}),
})
);
const closePopup = () => {
map.getOverlayById('map-popup').setPosition(undefined);
map.removeLayer(newMarkersLayer);
};
const [map] = useState(
new Map({
target: '',
layers: [
new TileLayer({
source: new OSM(),
}),
new VectorLayer({
properties: { name: 'existingMarkers' },
source: new VectorSource({
// features: [marker],
}),
}),
],
view: new View({
center: fromLonLat([26.08, 44.46]),
zoom: 15,
minZoom: 10,
maxZoom: 20,
}),
})
);
useEffect(() => {
const overlay = new Overlay({
element: popup.current,
id: 'map-popup',
autoPan: {
animation: {
duration: 250,
},
},
});
// console.log('useEffect in MapUI.jsx');
map.addOverlay(overlay);
map.setTarget(mapRef.current);
map.on('singleclick', function (evt) {
map.addLayer(newMarkersLayer);
newMarker.getGeometry().setCoordinates(evt.coordinate);
setCoordinates(toStringHDMS(toLonLat(evt.coordinate)));
overlay.setPosition(evt.coordinate);
});
}, [map]);
return (
<>
<div
style={{ height: '100%', width: '100%' }}
ref={mapRef}
className='map-container'
/>
<div id='map-popup' className={classes['ol-popup']} ref={popup}>
<PopUp coordinates={coordinates} closePopup={closePopup} />
</div>
</>
);
}
The project in the end will have an array of markers that will be requested from a back-end and will populate the given map while also keeping the ability to add new markers to the map (and to the back-end).
The general issue that I face is with how all ol objects are used in the documentation. Everything is just created in a file using const and then operated upon.
But in React I have to use useEffect() and useState() and can't just create dependencies or manipulate state however the docs say.
I am looking for some guidelines on how to properly use OpenLayers6 in React. On this note I have some questions:
How can I remove a marker without removing the whole layer ?
How can I make a marker stay on the map ?
How can I render an array or markers on the map ?
Is it correct the way I use useState() to create the initial map ?
Is it correct the way I use useState() to keep the marker and the VectorLayer on which the marker will be placed ?
Try using this library, Rlayers
this was very helpfull for me to combine with another gis library, like turf and d3
How can I remove a marker without removing the whole layer ?
You can use react-query as hooks (useMutation) fuction to call inside useCallback
How can I make a marker stay on the map ?
What are you meaning about stay? is like show and not hide? just get state and put that's value into true
How can I render an array or markers on the map ?
again use react-query
Is it correct the way I use useState() to create the initial map ?
Yes if you have another map on your application
Is it correct the way I use useState() to keep the marker and the VectorLayer on which the marker will be placed ?
Yes
As you know, OpenLayers uses an imperative API where you add and remove features to layers, etc., while in React land, we generally do things declaratively. You'll need some imperative glue code that takes your React state and mutates the OpenLayers state to match what your React state is - that's what React itself does when it reconciles the DOM.
You can do that in a couple of different ways:
useEffects to do what useEffect does: imperative side effects (updating OL state) based on declarative inputs (React state); described below
The other option, used by e.g. react-leaflet, is that you describe the whole Leaflet state as React elements, and the library reconciles the Leaflet state based on that. It's a lot more work, but feels more React-y.
Here's a pair of custom hooks that make things a bit easier, loosely based on your example. It's in TypeScript, but if you need plain JS, just remove the TypeScript specific things (by e.g. running this through the typescriptlang.org transpiler).
useOpenLayersMap is a hook that sets up an OL Map in a ref and returns it, and takes care that a given container renders the map.
useOpenLayersMarkerSource is a hook that receives a list of marker data (here quite a limited definition, but you could amend it as you like), returns a VectorSource and also takes care that modifications to the list of marker data are reflected into the VectorSource as new Features.
Now, all the MapUI component needs to do is use those two hooks with valid initial data; it can modify its markerData state and the changes get reflected into the map.
import React, { useRef, useState } from "react";
import "ol/ol.css";
import { Feature, Map, View } from "ol";
import Point from "ol/geom/Point";
import { Vector as VectorLayer } from "ol/layer";
import VectorSource from "ol/source/Vector";
import { fromLonLat } from "ol/proj";
import TileLayer from "ol/layer/Tile";
import OSM from "ol/source/OSM";
import { MapOptions } from "ol/PluggableMap";
interface MarkerDatum {
lat: number;
lon: number;
name: string;
}
function convertMarkerDataToFeatures(markerData: MarkerDatum[]) {
return markerData.map(
(md) =>
new Feature({
geometry: new Point(fromLonLat([md.lon, md.lat])),
name: md.name,
}),
);
}
const centerLat = 60.45242;
const centerLon = 22.27831;
function useOpenLayersMap(
mapDivRef: React.MutableRefObject<HTMLDivElement | null>,
getInitialOptions: () => Partial<MapOptions>,
) {
const mapRef = useRef<Map | null>(null);
React.useEffect(() => {
if (!mapRef.current) {
// markersSourceRef.current = new VectorSource();
mapRef.current = new Map({
target: mapDivRef.current ?? undefined,
...getInitialOptions(),
});
}
}, []);
React.useLayoutEffect(() => {
if (mapRef.current && mapDivRef.current) {
mapRef.current.setTarget(mapDivRef.current);
}
}, []);
return mapRef.current;
}
function useOpenLayersMarkerSource(markerData: MarkerDatum[]) {
const [markersSource] = useState(() => new VectorSource());
React.useEffect(() => {
// TODO: this would do better to only remove removed features,
// modify modified features and add new features
markersSource.clear(true);
markersSource.addFeatures(convertMarkerDataToFeatures(markerData));
}, [markerData]);
return markersSource;
}
export default function MapUI() {
const mapDivRef = useRef<HTMLDivElement>(null);
const [markerData, setMarkerData] = React.useState<MarkerDatum[]>([
{
lon: centerLon,
lat: centerLat,
name: "turku",
},
]);
const markerSource = useOpenLayersMarkerSource(markerData);
const map = useOpenLayersMap(mapDivRef, () => ({
layers: [
new TileLayer({
source: new OSM(),
}),
new VectorLayer({
properties: { name: "existingMarkers" },
source: markerSource,
}),
],
view: new View({
center: fromLonLat([centerLon, centerLat]),
zoom: 12,
minZoom: 12,
maxZoom: 20,
}),
}));
const addNewFeature = React.useCallback(() => {
setMarkerData((prevMarkerData) => [
...prevMarkerData,
{
lat: centerLat - 0.05 + Math.random() * 0.1,
lon: centerLon - 0.05 + Math.random() * 0.1,
name: "marker " + (0 | +new Date()).toString(36),
},
]);
}, []);
return (
<>
<button onClick={addNewFeature}>Add new feature</button>
<div
style={{ height: "800px", width: "800px" }}
ref={mapDivRef}
className="map-container"
/>
</>
);
}

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

React native no re-render after updating state array

I have the following code (full example):
import React, { useState, useEffect } from 'react';
import { SafeAreaView, View, Button, StyleSheet, Animated } from 'react-native';
import { PanGestureHandler, State } from 'react-native-gesture-handler';
const App = () => {
const [blocks, setBlocks] = useState([]);
const CreateBlockHandler = () => {
let array = blocks;
array.push({
x: new Animated.Value(0),
y: new Animated.Value(0)
});
setBlocks(array);
RenderBlocks();
};
const MoveBlockHandler = (index, event) => {
Animated.spring(blocks[index].x, { toValue: event.nativeEvent.x }).start();
Animated.spring(blocks[index].y, { toValue: event.nativeEvent.y }).start();
};
const RenderBlocks = () => {
return blocks.map((item, index) => {
return (
<PanGestureHandler key={index} onGestureEvent={event => MoveBlockHandler(index,event)}>
<Animated.View style={[styles.block, {
transform: [
{ translateX: item.x },
{ translateY: item.y }
]
}]} />
</PanGestureHandler>
)
});
};
return (
<SafeAreaView style={styles.container}>
<View style={styles.pancontainer}>
<RenderBlocks />
</View>
<Button title="Add block" onPress={CreateBlockHandler} />
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center'
},
pancontainer: {
width: '95%',
height:'75%',
borderWidth: 1,
borderColor: 'black'
},
block: {
width: 50,
height: 50,
backgroundColor: 'black'
}
});
export default App;
What does this code do? It's a big square, and a button below it. When I click on the button, a new black square (50x50) is made in the big square. I do this by creating a new array element (the array = blocks). This is done in the function CreateBlockHandler. This does not work correctly!
The function MoveBlockHandler makes the little squares movable. This works!
What does not work? When I create a new black square, the black square is not rendered on the screen. Only when I refresh, the square is rendered. The square is created through CreateBlockHandler, because when I do a console.log(blocks) in that function, I can see that a new array element is added.
How can I force this code to do a full re-render with all the array elements? I tried to wrap the render of the square in a separate function (RenderBlocks) and I'm calling this function every time a new square is made (last line in CreateBlockHandler). The function is called (I can check this with a console.log()) but no squares are rendered.
When you assign blocks to array the reference gete copied which mutates the state, so it doesn't re-render on setState.
const CreateBlockHandler = () => {
let array = [...blocks];
array.push({
x: new Animated.Value(0),
y: new Animated.Value(0)
});
setBlocks(array);
RenderBlocks
There are multiple issues with your code.
As kooskoos pointed out, your state remains referentially equal (it's the same array, only the elements change). This will not trigger re-render.
Also, you are manipulating state of the App component. RenderBlocks component's props and state remain unchanged which implies that they don't need to be re-rendered. Since the component is an anonymous function and is recreated during every render of App, it probably gets re-rendered anyways.
In addition, you are directly calling RenderBlocks, which looks like a component. That is unnecessary and will do nothing here, but if it had any hooks, it would cause problems.
You should probably also conform to the convention that components are PascalCase capitalised and callbacks snakeCase capitalised.

React-leaflet how to resetStyle

I'm following Leaflet's Choropleth tutorial
http://leafletjs.com/examples/choropleth.html
and using react-leaflet.
I managed to setStyle without any modification from the original source code and it works.
highlightFeature(e) {
var layer = e.target;
layer.setStyle({
weight: 5,
color: '#666',
dashArray: '',
fillOpacity: 0.7
});
}
The layer has a setStyle property. Now to resetStyle that I'm having propblems.
I tried to access it with
resetHighlight(e) {
this.refs.geojson.resetStyle(e.target);
}
while having GeoJson
<GeoJson
ref="geojson"
data={this.state.data}
style={this.getStyle.bind(this)}
onEachFeature={this.onEachFeature.bind(this)}
/>
but it it doesn't have resetStyle property
Anyone can suggest another way of resetting the style in react-leaflet ?
The solution was to access the leafletElement of geojson which has resetStyle
resetHighlight(e) {
this.refs.geojson.leafletElement.resetStyle(e.target);
}
react-leaflet-choropleth is a way to handle choropleth if you are not wanting to write it from scratch. It is based off of the leaflet-choropleth plugin
import Choropleth from 'react-leaflet-choropleth'
import { Map } from 'react-leaflet'
const style = {
fillColor: '#F28F3B', //default color filll
weight: 2, //normal styling
opacity: 1,
color: 'white',
dashArray: '3',
fillOpacity: 0.5
}
const map = (geojson) => (
<Map>
<Choropleth
data={{type: 'FeatureCollection', features: geojson} /*feature collection or array*/}
valueProperty={(feature) => feature.properties.value /*value for choropleth*/}
visible={(feature) => feature.id !== active.id /*use choropleth color?*/}
scale={['#b3cde0', '#011f4b'] /*color range*/}
steps={7 /*how many different colors to use?*/}
mode={'e' /*use equadistance mode, others include kmeans and quantile*/}
style={style}
onEachFeature={(feature, layer) => layer.bindPopup(feature.properties.label)}
ref={(el) => this.choropleth = el.leafletElement /*get the geojson's layer container*/}
/>
</Map>
)
ReactDom.render(<map geojson={...} />, document.body)

Resources