How to ensure ref.current exists before firing on click function? - reactjs

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} />

Related

How do I render canvas element on first render with typescript react?

I am using the canvas element to with request animation frame to give my website an animation that listens to a KeyboardEvent. The issue i'm dealing with is that when I load the page, or refresh the page it depends on the KeyboardEvent to load the fillRect on my canvas, rather than having the fillRect already on the canvas when my website loads or refreshes.
<canvas ref={canvasRef} className="landingPage__snakegame"></canvas>
const LandingPage: React.FC = () => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const ctx = canvasRef.current?.getContext('2d');
const [x,setX] = useState(100);
const [y,setY] = useState(50);
const handKeyPress = (event: KeyboardEvent) => {
if (event.key === "w") {
setY(y - 1)
}
}
console.log(canvasRef)
useEffect(() => {
window.addEventListener('keydown',handKeyPress);
return () => {
window.removeEventListener('keydown',handKeyPress)
}
})
const animateSquare = () => {
x + 1
ctx?.clearRect(0,0,canvasRef.current!.width,canvasRef.current!.height);
ctx?.fillRect(x,y,25,25);
requestAnimationFrame(animateSquare);
}
useEffect(() => {
animateSquare();
}, [y]);
I'm fairly sure that the main reason this is happening is because I have HTMLCanvasElement passed in as null, but I am unsure on how to change prevent due to TypeScript restrictions

How to pass ref to draw control in react-map-gl

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>
<>

React life cycles and Intersection observer

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]
);

React: Saved state updates the DOM but not the console

When I click, I set the saveMouseDown state to 1, when I release I set it to 0.
When I click and move the mouse I log out mouseDown and it's 0 even when my mouse is down? Yet on the screen it shows 1
import React, { useEffect, useRef, useState } from 'react';
const Home: React.FC = () => {
const [mouseDown, saveMouseDown] = useState(0);
const [canvasWidth, saveCanvasWidth] = useState(window.innerWidth);
const [canvasHeight, saveCanvasHeight] = useState(window.innerHeight);
const canvasRef = useRef<HTMLCanvasElement>(null);
let canvas: HTMLCanvasElement;
let ctx: CanvasRenderingContext2D | null;
const addEventListeners = () => {
canvas.addEventListener('mousedown', (e) => { toggleMouseDown(); }, true);
canvas.addEventListener('mouseup', (e) => { toggleMouseUp(); }, true);
};
const toggleMouseDown = () => saveMouseDown(1);
const toggleMouseUp = () => saveMouseDown(0);
const printMouse = () => console.log(mouseDown);
// ^------ Why does this print the number 1 and the 2x 0 and then 1... and not just 1?
const removeEventListeners = () => {
canvas.removeEventListener('mousedown', toggleMouseDown);
canvas.removeEventListener('mouseup', toggleMouseUp);
};
useEffect(() => {
if (canvasRef.current) {
canvas = canvasRef.current;
ctx = canvas.getContext('2d');
addEventListeners();
}
return () => removeEventListeners();
}, []);
useEffect(() => {
if (canvasRef.current) {
canvas = canvasRef.current;
canvas.addEventListener('mousemove', (e) => { printMouse(); }, true );
}
return () => canvas.removeEventListener('mousemove', printMouse);
}, [mouseDown, printMouse]);
return (
<React.Fragment>
<p>Mouse Down: {mouseDown}</p>
{/* ^------ When this does print 1? */}
<canvas
id='canvas'
ref={canvasRef}
width={canvasWidth}
height={canvasHeight}
/>
</React.Fragment>
);
};
export { Home };
You only add the move listener once when the component mounted, thus enclosing the initial mouseDown value.
Try using a second useEffect hook to specifically set/update the onMouseMove event listener when the mouseDown state changes. The remove eventListener needs to specify the same callback.
useEffect(() => {
if (canvasRef.current) {
canvas = canvasRef.current;
canvas.addEventListener('mousemove', printMouse, true );
}
return () => canvas.removeEventListener('mousemove', printMouse);;
}, [mouseDown, printMouse]);
It may be simpler to attach the event listeners directly on the canvas element, then you don't need to worry about working with enclosed stale state as much with the effect hooks.
<canvas
onMouseDown={() => setMouseDown(1)}
onMouseUp={() => setMouseDown(0)}
onMouseMove={printMouse}
width={canvasWidth}
height={canvasHeight}
/>

When to use hooks? is worth it that example?

I have write a hook to check if browser is IE, so that I can reutilize the logic instead of write it in each component..
const useIsIE = () => {
const [isIE, setIsIE] = useState(false);
useEffect(() => {
const ua = navigator.userAgent;
const isIe = ua.indexOf("MSIE ") > -1 || ua.indexOf("Trident/") > -1;
setIsIE(isIe);
}, []);
return isIE;
}
export default useIsIE;
Is it worth it to use that hook?
Im not sure if is good idea because that way, Im storing a state and a effect for each hook call (bad performane?) when I can simply use a function like that:
export default () => ua.indexOf("MSIE ") > -1 || ua.indexOf("Trident/") > -1;
What do you think? is worth it use that hook or not?
If not, when should I use hooks and when not?
ty
No. Not worth using the hook.
You'd need to use a hook when you need to tab into React's underlying state or lifecycle mechanisms.
Your browser will probably NEVER change during a session so just creating a simple utility function/module would suffice.
I would recommend to set your browser checks in constants and not functions, your browser will never change.
...
export const isChrome = /Chrome/.test(userAgent) && /Google Inc/.test(navigator.vendor);
export const isIOSChrome = /CriOS/.test(userAgent);
export const isMac = (navigator.platform.toUpperCase().indexOf('MAC') >= 0);
export const isIOS = /iphone|ipad|ipod/.test(userAgent.toLowerCase());
...
This is a simple hook that checks if a element has been scrolled a certain amount of pixels
const useTop = (scrollable) => {
const [show, set] = useState(false);
useEffect(() => {
const scroll = () => {
const { scrollTop } = scrollable;
set(scrollTop >= 50);
};
const throttledScroll = throttle(scroll, 200);
scrollable.addEventListener('scroll', throttledScroll, false);
return () => {
scrollable.removeEventListener('scroll', throttledScroll, false);
};
}, [show]);
return show;
};
Then you can use it in a 'To Top' button to make it visible
...
import { tween } from 'shifty';
import useTop from '../../hooks/useTop';
// scrollRef is your scrollable container ref (getElementById)
const Top = ({ scrollRef }) => {
const t = scrollRef ? useTop(scrollRef) : false;
return (
<div
className={`to-top ${t ? 'show' : ''}`}
onClick={() => {
const { scrollTop } = scrollRef;
tween({
from: { x: scrollTop },
to: { x: 0 },
duration: 800,
easing: 'easeInOutQuart',
step: (state) => {
scrollRef.scrollTop = state.x;
},
});
}}
role="button"
>
<span><ChevronUp size={18} /></span>
</div>
);
};

Resources