Following the Mapbox draw example I can use the draw variable to access all features that are drawn on a map.
const draw = new MapboxDraw({
// ...
});
map.addControl(draw);
// ...
function updateArea(e) {
const data = draw.getAll(); // Accessing all features (data) drawn here
// ...
}
However, in react-map-gl library useControl example I can not figure out how to pass ref to the DrawControl component so I can use it as something like draw.current in a similar way as I did draw in normal javascript above.
In my DrawControl.jsx
const DrawControl = (props) => {
useControl(
({ map }) => {
map.on('draw.create', props.onCreate);
// ...
return new MapboxDraw(props);
},
({ map }) => {
map.off('draw.create', props.onCreate);
// ...
},{
position: props.position,
},
);
return null;
};
In my MapDrawer.jsx
import Map from 'react-map-gl';
import DrawControl from './DrawControl';
// ...
export const MapDrawer = () => {
const draw = React.useRef(null);
const onUpdate = React.useCallback((e) => {
const data = draw.current.getAll(); // this does not work as expected
// ...
}, []);
return (
// ...
<Map ...>
<DrawControl
ref={draw}
onCreate={onUpdate}
onUpdate={onUpdate}
...
/>
</Map>
)
}
I also get an error stating I should use forwardRef but I'm not really sure how.
react_devtools_backend.js:3973 Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
What I need is basically to delete the previous feature if there is a new polygon drawn on a map so that only one polygon is allowed on a map. I want to be able to do something like this in the onUpdate callback.
const onUpdate = React.useCallback((e) => {
// ...
draw.current.delete(draw.current.getAll.features[0].id);
// ...
}, []);
I had the similar problem recently with that lib, I solved it doing the following :
export let drawRef = null;
export default const DrawControl = (props) => {
drawRef = useControl(
({ map }) => {
map.on('draw.create', props.onCreate);
// ...
return new MapboxDraw(props);
},
({ map }) => {
map.off('draw.create', props.onCreate);
// ...
},{
position: props.position,
},
);
return null;
};
import DrawControl, {drawRef} from './DrawControl';
// ...
export const MapDrawer = () => {
const draw = drawRef;
const onUpdate = React.useCallback((e) => {
const data = draw?draw.current.getAll():null; // this does not work as expected
// ...
}, []);
return (
// ...
<Map ...>
<DrawControl
onCreate={onUpdate}
onUpdate={onUpdate}
...
/>
</Map>
)
}
const onUpdate = React.useCallback((e) => {
// ...
drawRef.delete(drawRef.getAll.features[0].id);
// ...
}, []);
Once component created, the ref is available for use.
Not that elegant but working... Sure there might be cleaner way...
Hope that helps!
Cheers
Pass draw from draw control to parent component.
const DrawControl = (props) => {
const [draw, setDraw] = useState()
const { setDraw: setDrawInParent, onUpdate, onCreate, onDelete } = props;
useEffect(() => {
if (draw) setDrawInParent(draw)
}, [draw])
useControl(
({ map }) => {
map.on("draw.create", onCreate);
map.on("draw.update", onUpdate);
map.on("draw.delete", onDelete);
const draw = new MapboxDraw(props);
setDraw(draw);
return draw;
}
);
return null;
};
I think I found a better solution combine forwardRef and useImperativeHandle to solve:
export const DrawControl = React.forwardRef((props: DrawControlProps, ref) => {
const drawRef = useControl<MapboxDraw>(
() => new MapboxDraw(props),
({ map }) => {
map.on("draw.create", props.onCreate);
map.on("draw.update", props.onUpdate);
map.on("draw.delete", props.onDelete);
map.on("draw.modechange", props.onModeChange);
},
({ map }) => {
map.off("draw.create", props.onCreate);
map.off("draw.update", props.onUpdate);
map.off("draw.delete", props.onDelete);
map.off("draw.modechange", props.onModeChange);
},
{
position: props.position,
}
);
React.useImperativeHandle(ref, () => drawRef, [drawRef]); // This way I exposed drawRef outside the component
return null;
});
in the component:
const drawRef = React.useRef<MapboxDraw>();
const [drawMode, setDrawMode] = React.useState<DrawMode>(“draw_polygon");
const changeModeTo = (mode: DrawMode) => {
// If you programmatically invoke a function in the Draw API, any event that directly corresponds with that function will not be fired
drawRef.current?.changeMode(mode as string);
setDrawMode(mode);
};
<>
<DrawControl
ref={drawRef}
position="top-right”
displayControlsDefault={false}
controls={{
polygon: true,
trash: true,
}}
defaultMode=“draw_polygon"
onCreate={onUpdate}
onUpdate={onUpdate}
onDelete={onDelete}
onModeChange={onModeChange}
/>
<button
style={{
position: ‘absolute’,
left: ‘20px’,
top: ‘20px’,
backgroundColor: '#ff0000’,
}}
onClick={() => changeModeTo('simple_select’)}
>
Change to Simple Select
</button>
<>
Related
This is not a Mapbox related issue, and you don’t need any map or related stuff knowledge to help. My issue is related to React refs.
I’m using react-map-gl (Mapbox for React) with #mapbox/mapbox-gl-draw that allow user to draw geometries on the map. This app has 2 components: DrawControl that exposes draw features and AllPastures that renders the map and manage create/update/delete geometries drawed.
When user draws a polygon it fires the DrawControl method onCreate that executes onCreateOrUpdate. I’m passing the Draw by forwardRef from DrawControl to AllPastures.
This is a DrawControl component:
export const DrawControl = React.forwardRef<MapboxDraw, DrawControlProps>(
(props: DrawControlProps, ref) => {
const drawRef: MapboxDraw = useControl<MapboxDraw>(
() => new MapboxDraw(props),
({ map }) => {
map.on('draw.create', props.onCreate);
map.on('draw.update', props.onUpdate);
map.on('draw.delete', props.onDelete);
map.on('draw.modechange', props.onModeChange);
},
({ map }) => {
map.off('draw.create', props.onCreate);
map.off('draw.update', props.onUpdate);
map.off('draw.delete', props.onDelete);
map.off('draw.modechange', props.onModeChange);
},
{
position: props.position,
}
);
// exposing drawRef outside the component by Ref
React.useImperativeHandle(ref, () => drawRef, [drawRef]);
return null;
}
and this is part of AllPastures component:
export const AllPastures = () => {
...
const [drawRef, setDrawRef] = React.useState<MapboxDraw>();
// here I’m using callback ref to react to drawRef changes
const onDrawRef = React.useCallback((ref: MapboxDraw | null) => {
if (ref) {
setDrawRef(ref);
}
}, []);
React.useEffect(() => {
// it's ok here, drawRef is not undefined
console.log('useEffect drawRef when app is loading', drawRef);
}, [map, drawRef]);
const onCreateOrUpdate = (e: { features: Feature[] }) => {
// Why drawRef is undefined here????????????????????
console.log('drawRef under onCreateOrUpdate method', drawRef);
};
...
return (
<DrawControl
ref={onDrawRef}
position="top-right"
displayControlsDefault={false}
controls={{
polygon: true,
trash: true,
}}
defaultMode="simple_select"
onCreate={onCreateOrUpdate}
onUpdate={onCreateOrUpdate}
/>
);
};
My issue is, discover why when the the method onCreateOrUpdate the drawRef is undefined?
Here is a related sandbox simulating the issue:
https://stackblitz.com/edit/vitejs-vite-bvutvb?file=src%2FAllPastures.tsx
Just draw any polygon on the map and check the console.log drawRef under onCreateOrUpdate method is undefined.
Please, after make changes in the code, do a F5 to complete refresh the page and test it again.
const onCreateOrUpdate = React.useCallback((e: { features: Feature[] }) => {
// Here drawRef would not be undefined
console.log('drawRef under onCreateOrUpdate method', drawRef);
}, [drawRef]);
I am quite new to Arcgis-JS and React. As suggested here, I am using a functional component with the useEffect hook to integrate my map.
Now I want to display a line within my map when I click on a certain row of a list. On click I am fetching the appropriate coordinates to be displayed and storing them to a context-variable (dbPageContext.currentGeom).
The problem: When I want to display another line, the entire map-component has to re-render as I am passing the line-array-variable as a second argument to the useEffect hook.
const MapComp = () => {
const mapRef = useRef();
const dbPageContext = useContext(DbPageContext);
useEffect(() => {
const mainMap = new Map({
layers: [layer],
basemap: "arcgis-topographic", // Basemap layer service
});
const graphicsLayer = new GraphicsLayer();
mainMap.add(graphicsLayer);
const polyline = {
type: "polyline",
paths: dbPageContext.currentGeom,
};
const simpleLineSymbol = {
type: "simple-line",
color: [0, 230, 250],
width: 4,
};
const polylineGraphic = new Graphic({
geometry: polyline,
symbol: simpleLineSymbol,
});
graphicsLayer.add(polylineGraphic);
const view = new MapView({
container: mapRef.current,
map: mainMap,
spatialReference: {
wkid: 3857,
},
});
return () => {
view && view.destroy();
};
}, [dbPageContext.currentGeom]);
return (
<div>
<div className="webmap" ref={mapRef} />
</div>
);
};
export default MapComp;
How can I update only the graphics-layer without updating the entire map-component? Would be great if someone could help me finding a solution for that.
EDIT: I also tried to implement the map without using the useeffect hook. But then, nothing was displayed.
You need to separate the effects. On page load, you should have one effect that creates the map. Then a second effect can update the map when dbPageContext.currentGeom changes.
const MapComp = () => {
const mapRef = useRef();
const dbPageContext = useContext(DbPageContext);
// Memoize this, as you only need to create it once, but you also need
// it to be available within scope of both of the following useEffects
const graphicsLayer = React.useMemo(
() => new GraphicsLayer(),
[]
);
// On mount, create the map, view, and teardown
useEffect(() => {
const mainMap = new Map({
layers: [layer],
basemap: "arcgis-topographic", // Basemap layer service
});
const view = new MapView({
container: mapRef.current,
map: mainMap,
spatialReference: {
wkid: 3857,
},
});
mainMap.add(graphicsLayer);
return () => {
view && view.destroy();
};
}, [])
// When dbPageContext.currentGeom changes, add a polyline
// to the graphics layer
useEffect(() => {
const polyline = {
type: "polyline",
paths: dbPageContext.currentGeom,
};
const simpleLineSymbol = {
type: "simple-line",
color: [0, 230, 250],
width: 4,
};
const polylineGraphic = new Graphic({
geometry: polyline,
symbol: simpleLineSymbol,
});
// Clear previously added lines (if that's what you want)
graphicsLayer.removeAll()
graphicsLayer.add(polylineGraphic);
}, [dbPageContext.currentGeom]);
return (
<div>
<div className="webmap" ref={mapRef} />
</div>
);
};
export default MapComp;
I am building a image slider in React, based on CSS vertical snapping. There are 2 ways to interact with it, either throught scroll vertically or click the navigation buttons. I am using the Intersection Observer API in a React useEffect() to detect the active item. However, I can't seem to get it right without any useEffect lint errors. Whenever I include the functions in the dependecy array as suggested by the lint, the active item isn't set when scrolling.
Am I using a React anti pattern or am I just missing something?
Live demo
Code:
const Slider = ({images}) => {
const [currentSlide, SetCurrentSlide] = React.useState(0);
const setSlide = (id) => {
SetCurrentSlide(id);
};
const moveToSlide = (id) => {
if(id > -1 && id < images.length) {
SetCurrentSlide(id);
}
}
return (
<StyledSlider id="slider">
<SliderWrapper items={images} setSlide={setSlide} currentSlide={currentSlide} />
<SliderNav currentSlide={currentSlide} moveToSlide={moveToSlide} maxItems={images.length}/>
</StyledSlider>
)
}
const SliderWrapper = ({items, setSlide, currentSlide}) => {
const containerRef = React.useRef(null);
const { ref, inView, entry } = useInView({
/* Optional options */
threshold: 0,
});
const handleSetSlide = (id) => {
setSlide(id);
};
const handleIntersection = (entries) => {
const [entry] = entries;
const activeSlide = Number(entry.target.dataset.slide);
if (!entry.isIntersecting || activeSlide === "NaN") return;
handleSetSlide(activeSlide);
};
React.useEffect(() => {
const observer = new IntersectionObserver(
handleIntersection,
{
root: containerRef.current,
threshold: 0.45
}
);
Array.from(containerRef.current.children).forEach((item) => {
observer.observe(item);
});
return function() {
observer.disconnect();
}
}, [items]);
return (
<StyledSliderWrapper ref={containerRef} >
{items.map((item, index) => {
return <SliderItem key={index} index={index} image={item} isActive={currentSlide === index} />
})}
</StyledSliderWrapper>
)
};
const SliderItem = ({index, image, isActive}) => {
const imageContent = getImage(image.url);
const imageRef = React.useRef()
React.useEffect(() => {
if(!isActive) return;
imageRef.current.scrollIntoView({behavior: "smooth", block: "center", inline: "center"});
},[isActive]);
return (
<StyledSliderItem data-slide={index} ref={imageRef}>
<GatsbyImage image={imageContent} alt={image.description} />
</StyledSliderItem>
)
}
So you've missing dependencies in the useEffect of SliderWrapper. You can simplify the code a bit as well.
SliderWrapper
Since nothing else calls handleIntersection callback other than the Observer you can safely move it into the useEffect callback body. This makes the only dependency the setSlide callback that's passed as a prop from the parent component.
const SliderWrapper = ({ items, setSlide, currentSlide }) => {
const containerRef = React.useRef(null);
React.useEffect(() => {
const handleIntersection = (entries) => {
const [entry] = entries;
const activeSlide = Number(entry.target.dataset.slide);
if (!entry.isIntersecting || activeSlide === "NaN") return;
setSlide(activeSlide);
};
const observer = new IntersectionObserver(handleIntersection, {
root: containerRef.current,
threshold: 0.45
});
Array.from(containerRef.current.children).forEach((item) => {
observer.observe(item);
});
return function () {
observer.disconnect();
};
}, [setSlide]);
return (
<StyledSliderWrapper ref={containerRef}>
{items.map((item, index) => (
<SliderItem
key={index}
index={index}
image={item}
isActive={currentSlide === index}
/>
))}
</StyledSliderWrapper>
);
};
Slider
The other issue what that you were memoizing the setSlide prop in the child instead of the parent where it's being passed down. This caused the setSlide prop to be a new reference each render and re-memoized via useCallback in the child. React useState updater functions are stable however, so you can directly pass them to children.
const Slider = ({ images }) => {
const [currentSlide, setCurrentSlide] = React.useState(0);
const moveToSlide = (id) => {
setCurrentSlide(id);
};
return (
<StyledSlider id="slider">
<SliderWrapper
items={images}
setSlide={setCurrentSlide} // <-- pass directly to child
currentSlide={currentSlide}
/>
<SliderNav
currentSlide={currentSlide}
moveToSlide={moveToSlide}
maxItems={images.length}
/>
</StyledSlider>
);
};
If you wanted to remain with the setSlide handler in the parent, here is where you'd memoize the callback so the parent is providing a stable reference. Note that this is only useful if memoizing non-useState functions.
const setSlide = React.useCallback(
(id) => {
setCurrentSlide(id);
},
[setCurrentSlide]
);
const AnimatedText = Animated.createAnimatedComponent(Text);
function Component({ texts }) {
const [visitIndex, setVisitIndex] = React.useState(0);
// can't create an array of shared value for each text
// since useSharedValue is a hook, and that throws a warning
const textScalesShared = texts.map((_) => useSharedValue(1));
// can't create an array of animated style for each text
// since useAnimatedStyle is a hook, and that throws a warning
const animatedTextStyle = textScalesShared.map((shared) =>
useAnimatedStyle(() => ({
transform: [{ scale: shared.value }],
}))
);
useEffect(() => {
// code to reduce text scale one after another
// it will loop over the array of textScaleShared values
// passed to each component and update it
if (visitIndex === texts.length) {
return;
}
textScalesShared[visitIndex].value = withDelay(
1000,
withTiming(0.5, {
duration: 1000,
})
);
const timerId = setTimeout(() => {
setVisitIndex((idx) => idx + 1);
}, 1000);
return () => {
clearTimeout(timerId);
};
}, [visitIndex]);
return texts.map((text, index) => {
if (index <= visitIndex) {
return (
<AnimatedRevealingText
key={index}
fontSize={fontSize}
revealDuration={revealDuration}
style={animatedStylesShared[index]}
{...props}
>
{text}
</AnimatedRevealingText>
);
} else {
return null;
}
});
}
I want to apply animated styles to an array of components, but since useSharedValue and useAnimatedStyle are both hooks, I am unable to loop over the prop and create a shared value and the corresponding style for each of the component.
How can I achieve the same?
EDIT: updated to add the full code.
You can create a component to handle the useSharedValue and useAnimatedStyle hooks for every item using the visitIndex value:
AnimatedTextItem.js
const AnimatedText = Animated.createAnimatedComponent(Text);
const AnimatedTextItem = ({text, visited}) => {
const textScaleShared = useSharedValue(1);
const style = useAnimatedStyle(() => ({
transform: [{ textScaleShared.value }],
}));
useEffect(()=> {
if(visited) {
textScaleShared.value = withDelay(
1000,
withTiming(0.5, {
duration: 1000,
});
);
}
}, [visited]);
return (<AnimatedText style={style}>{text}</AnimatedText>)
}
Component.js
function Component({texts}) {
const [visitIndex, setVisitIndex] = React.useState(0);
useEffect(() => {
// code to reduce text scale one after another
// it will loop over the array of textScaleShared values
// passed to each component and update it
if (visitIndex === texts.length) {
return;
}
const timerId = setTimeout(() => {
setVisitIndex((idx) => idx + 1);
}, revealDuration);
return () => {
clearTimeout(timerId);
};
}, []);
return texts.map((text, index) => (<AnimatedTextItem text={text} visited={visitIndex === index}/>))
}
You can compose a component to handle it for you, but you need to pass the index of the text you're mapping through.
Like this
const AnimatedText = ({styleIndex}) => {
const textScaleShared = useSharedValue(styleIndex + 1);
const animatedTextStyle = useAnimatedStyle(() => ({
transform: [{ scale: textScaleShared.value }],
}));
const Animated = Animated.createAnimatedComponent(Text);
return <Animated style={animatedTextStyle}>{text}</Animated>;
};
function Component({ texts }) {
useEffect(() => {
// code to reduce text scale one after another
}, []);
return texts.map((text, index) => (
<AnimatedText key={index} styleIndex={index}>
{text}
</AnimatedText>
));
}
Interesting problem :) Let me see if i can come up a solution.
You already notice hook can't be in a dynamic array since the length of array is unknown.
Multiple components
You can have as many as components as you want, each one can have a hook, ex.
const Text = ({ text }) => {
// useSharedValue(1)
// useAnimatedStyle
}
const Components = ({ texts }) => {
return texts.map(text => <Text text={text} />)
}
Single hook
You can also see if you can find a className that can apply to all components at the same time. It's css i assume.
I am receiving an undefined error when trying to set canvasRef.current. I have tried many different ways to form a callback ref, but I am getting no luck. How can I wait to fire the onClick function 'handleViewStuff' AFTER canvasRef.current is not undefined?
const Child = (props) => {
const canvasRef = useRef();
const handleViewStuff = useCallback(() => {
apiCall(id)
.then((response) => {
// do stuff
return stuff;
})
.then((result) => {
result.getPage().then((page) => {
const canvas = canvasRef.current;
const context = canvas.getContext('2d'); // error is coming in here as getContext of undefined meaning canvas is undefined'
canvas.height = 650;
const renderContext = {
canvasContext: context,
};
page.render(renderContext);
});
});
}, []);
return (
<Fragment>
<canvas ref={(e) => {canvasRef.current = e}} />
<Button
onClick={handleViewStuff}
>
View Stuff
</Button>
</Fragment>
);
};
export default Child;
Using if-statement
...
if(canvas.current) {
const canvas = canvasRef.current;
const context = canvas.getContext('2d');
}
Using optional chaining
...
const canvas = canvasRef?.current;
const context = canvas?.getContext('2d');
And I found some mistakes in your code.
add dependencies on useCallback
const handleViewStuff = useCallback(() => {
...
}, [canvasRef.current]);
should use ref like this.
<canvas ref={canvasRef} />