React ref is not set correctly inside useEffect - javascript

I have a parent component that passes its size as a prop and renders a child component containing a canvas. It looks like this :
const CanvasContainer = () => {
const containerRef = useRef(null);
const [canvasWidth, setCanvasWidth] = useState(0);
const [canvasHeight, setCanvasHeight] = useState(0);
useLayoutEffect(() => {
const container = containerRef.current;
if (!container) return;
setCanvasWidth(container.offsetWidth);
setCanvasHeight(container.offsetHeight);
}, []);
return (
<div ref={containerRef}>
<Canvas canvasWidth={canvasWidth} canvasHeight={canvasHeight} />
</div>
);
};
The child component saves a ref to the canvas, creates the canvas context, modifies some properties of the canvas context, and saves this context to another ref.
However, when the context gets saved to the ref, it is saved with its default properties instead of the properties that I had modified.
const Canvas = (props) => {
const canvasRef = useRef(null);
const contextRef = useRef(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const context = canvas.getContext('2d');
if (!context) return;
context.lineCap = 'round';
console.log(contextRef.current); // Logs null
contextRef.current = context;
console.log(contextRef.current); // Logs CanvasRenderingContext2D but lineCap is still set to its default value ('butt' instead of 'round')
}, []);
const { canvasWidth, canvasHeight } = props;
return (
<canvas
ref={canvasRef}
width={canvasWidth}
height={canvasHeight}
/>
);
};
I tried to replace useEffect with useLayoutEffect but it doesn't change anything.
I noticed that this bug is not occurring when I add canvasWidth and canvasHeight to the useEffect dependency array.
Is this behavior expected or is this some kind of bug ?
Live Example:
const {useRef, useState, useEffect, useLayoutEffect} = React;
const CanvasContainer = () => {
const containerRef = useRef(null);
const [canvasWidth, setCanvasWidth] = useState(0);
const [canvasHeight, setCanvasHeight] = useState(0);
useLayoutEffect(() => {
const container = containerRef.current;
if (!container) return;
setCanvasWidth(container.offsetWidth);
setCanvasHeight(container.offsetHeight);
}, []);
return (
<div ref={containerRef}>
<Canvas canvasWidth={canvasWidth} canvasHeight={canvasHeight} />
</div>
);
};
const Canvas = (props) => {
const canvasRef = useRef(null);
const contextRef = useRef(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const context = canvas.getContext('2d');
if (!context) return;
context.lineCap = 'round';
console.log(context.lineCap);
console.log(contextRef.current); // Logs null
contextRef.current = context;
console.log(contextRef.current); // Logs CanvasRenderingContext2D but lineCap is still set to its default value ('butt' instead of 'round')
console.log(contextRef.current.lineCap);
}, []);
const [isDrawing, setIsDrawing] = useState(false);
const startDrawing = (event) => {
const { offsetX, offsetY } = event.nativeEvent;
if (!contextRef.current) return
console.log(contextRef.current.lineCap); // Logs 'butt' instead of 'round'
contextRef.current.beginPath();
contextRef.current.moveTo(offsetX, offsetY);
setIsDrawing(true);
};
const draw = (event) => {
if (!isDrawing) return;
const { offsetX, offsetY } = event.nativeEvent;
if (!contextRef.current) return
contextRef.current.lineTo(offsetX, offsetY);
contextRef.current.stroke();
};
const stopDrawing = () => {
if (!contextRef.current) return
contextRef.current.closePath();
setIsDrawing(false);
};
const { canvasWidth, canvasHeight } = props;
return (
<canvas
ref={canvasRef}
width={canvasWidth}
height={canvasHeight}
// Event Handlers
onMouseDown={startDrawing}
onMouseMove={draw}
onMouseUp={stopDrawing}
/>
);
};
ReactDOM.render(<CanvasContainer />, document.getElementById("root"));
#root > div {
height: 200px;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

Related

Calling function when useState s called?

Parent Component
const TopPage = (props) =>{
const [fileUrl, setFileUrl ] = useState("");
const onSetFileUrl = (data) =>{
setFileUrl(data);
}
return (
<div>
<ImagePreview fileUrl={fileUrl}></ImagePreview>
</div>
);
}
Child Component
import React from 'react'
const ImagePreview = (props) =>{
const layer = null;
const onImageChange = () =>{
var layer = document.getElementById('layer');
var ctx = layer.getContext('2d');
console.log("image is changed");
const chara = new Image();
chara.src = props.fileUrl;
chara.onload = () => {
ctx.drawImage(chara, 0, 0);
};
}
return (<div>Image Preview:{props.fileUrl}
<canvas id="layer" style={{width: 100, height: 100,backgroundColor:"yellow"}} />
</div>);
};
export default ImagePreview;
Parent Component gives the fileUrl to Child Component.
When data changed, fileUrl is given to Child Component via props.
However, at the same time I want to draw this in canvas.
So I need to call onImageChange() of Child Component when fileUrl is changed.
How can I do this ?

react, To call a function from a child component to a parent component

I want to run a function on the parent component in the child component.
Eventually, I want to have the function run when the scroll goes to that position.
UseImperativeHandle was used, but props did not apply. Is there a way to apply props in the useImperativeHandle?
Also, is it correct to use IntersectionObserver this way?
child Components
function Percent(props, ref) {
useImperativeHandle(ref,() => ({
percentst: () => {
var cnt = document.querySelectorAll(".count")[props.num];
var water = document.querySelectorAll(".water")[props.num];
var percent = cnt.innerText;
var interval;
interval = setInterval(function () {
percent++;
cnt.innerHTML = percent;
water.style.transform = 'translate(0' + ',' + (100 - percent) + '%)';
if (percent == props.percent) {
clearInterval(interval);
}
}, 80);
}
}));
}
export default forwardRef(Percent);
parent component
function About(props) {
const containerRef = useRef();
const myRef = useRef();
const [isVisible, setIsVisible] = useState(false);
const callbackFunction = (entries) => {
const [entry] = entries;
setIsVisible(entry.isIntersecting);
};
const options = {
root: document.getElementById('skills'),
rootMargin: '0px',
threshold: 1
};
useEffect(() => {
const observer = new IntersectionObserver(callbackFunction, options);
console.log(containerRef.current)
if (containerRef.current) observer.observe(containerRef.current);
return () => {
myRef.current.percentst()
if (containerRef.current) observer.unobserve(containerRef.current);
};
}, [containerRef, options]);
return(
<div ref={containerRef}></div>
<Percent ref={myRef} />
)
}
export default About;
Two ways of doing this
Pass your method as a prop to the child (this may not be desirable)
Use custom event listeners/triggers
There are some packages out there that will help with events such as https://www.npmjs.com/package/react-custom-events

Using array of Refs intead of single Ref

I'm using ref inside a map loop.
I need an array of refs
The problem is the ref only target the last element generated in the list
here is the example I prepared,
I need to have the custom hook run on all generated elements inside the map loop by a list of ref
I'm looking for a way without introducing another Component
import React, { useRef, useState, useEffect, useCallback } from "react";
/// throttle.ts
export const throttle = (f) => {
let token = null,
lastArgs = null;
const invoke = () => {
f(...lastArgs);
token = null;
};
const result = (...args) => {
lastArgs = args;
if (!token) {
token = requestAnimationFrame(invoke);
}
};
result.cancel = () => token && cancelAnimationFrame(token);
return result;
};
const id = (x) => x;
const useDraggable = ({ onDrag = id } = {}) => {
const [pressed, setPressed] = useState(false);
const position = useRef({ x: 0, y: 0 });
const ref = useRef();
const unsubscribe = useRef();
const legacyRef = useCallback((elem) => {
ref.current = elem;
if (unsubscribe.current) {
unsubscribe.current();
}
if (!elem) {
return;
}
const handleMouseDown = (e) => {
e.target.style.userSelect = "none";
setPressed(true);
};
elem.addEventListener("mousedown", handleMouseDown);
unsubscribe.current = () => {
elem.removeEventListener("mousedown", handleMouseDown);
};
}, []);
useEffect(() => {
if (!pressed) {
return;
}
const handleMouseMove = throttle((event) => {
if (!ref.current || !position.current) {
return;
}
const pos = position.current;
const elem = ref.current;
position.current = onDrag({
x: pos.x + event.movementX,
y: pos.y + event.movementY
});
elem.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
});
const handleMouseUp = (e) => {
e.target.style.userSelect = "auto";
setPressed(false);
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
return () => {
handleMouseMove.cancel();
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}, [pressed, onDrag]);
return [legacyRef, pressed];
};
/// example.ts
const quickAndDirtyStyle = {
width: "200px",
height: "200px",
background: "#FF9900",
color: "#FFFFFF",
display: "flex",
justifyContent: "center",
alignItems: "center"
};
const DraggableComponent = () => {
const handleDrag = useCallback(
({ x, y }) => ({
x: Math.max(0, x),
y: Math.max(0, y)
}),
[]
);
const [ref, pressed] = useDraggable({
onDrag: handleDrag
});
return (
<>
{[1, 2, 3].map((el, i) => (
<div key={"element" + i} ref={ref} style={quickAndDirtyStyle}>
<p>{pressed ? "Dragging..." : "Press to drag"}</p>
</div>
))}
</>
);
};
export default function App() {
return (
<div className="App">
<DraggableComponent />
</div>
);
}
a link to a codesandbox is here
https://codesandbox.io/s/determined-wave-pfklec?file=/src/App.js
Assuming that the goal is to make each generated element draggable individually, here is an example by switching some ref to arrays, and changed pressed to number | boolean to pass an index.
Changed the names of legacyRef and pressed to handleRefs and pressedIndex to reflect the difference in their use case.
Forked live demo on: codesandbox (updated to omit the use of useCallback)
However, with the hook applied, it seems that each element (except the first one) has a limited draggable area.
The posted example also has this behavior on the third draggable item, so not sure if this is intended by the hook. If not, perhaps the implement of draggable need to be adjusted to be fit for all elements.
Hope that this could help as a reference.
import React, { useRef, useState, useEffect } from "react";
/// throttle.ts
export const throttle = (f) => {
let token = null,
lastArgs = null;
const invoke = () => {
f(...lastArgs);
token = null;
};
const result = (...args) => {
lastArgs = args;
if (!token) {
token = requestAnimationFrame(invoke);
}
};
result.cancel = () => token && cancelAnimationFrame(token);
return result;
};
const id = (x) => x;
const useDraggable = ({ onDrag = id } = {}) => {
const [pressedIndex, setPressedIndex] = useState(false);
const positions = useRef([]);
const refs = useRef([]);
const unsubscribes = useRef([]);
const handleRefs = (elem, i) => {
if (!elem) {
return;
}
refs.current[i] = elem;
if (!positions.current[i]) positions.current[i] = { x: 0, y: 0 };
if (unsubscribes.current[i]) {
unsubscribes.current[i]();
}
const handleMouseDown = (e) => {
e.target.style.userSelect = "none";
setPressedIndex(i);
};
elem.addEventListener("mousedown", handleMouseDown);
unsubscribes.current[i] = () => {
elem.removeEventListener("mousedown", handleMouseDown);
};
};
useEffect(() => {
if (!pressedIndex && pressedIndex !== 0) {
return;
}
const handleMouseMove = throttle((event) => {
if (
!refs.current ||
refs.current.length === 0 ||
!positions.current ||
positions.current.length === 0
) {
return;
}
const pos = positions.current[pressedIndex];
const elem = refs.current[pressedIndex];
positions.current[pressedIndex] = onDrag({
x: pos.x + event.movementX,
y: pos.y + event.movementY
});
elem.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
});
const handleMouseUp = (e) => {
e.target.style.userSelect = "auto";
setPressedIndex(false);
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
return () => {
handleMouseMove.cancel();
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
}, [pressedIndex, onDrag]);
return [handleRefs, pressedIndex];
};
/// example.ts
const quickAndDirtyStyle = {
width: "200px",
height: "200px",
background: "#FF9900",
color: "#FFFFFF",
display: "flex",
justifyContent: "center",
alignItems: "center"
};
const DraggableComponent = () => {
const handleDrag = ({ x, y }) => ({
x: Math.max(0, x),
y: Math.max(0, y)
});
const [handleRefs, pressedIndex] = useDraggable({
onDrag: handleDrag
});
return (
<>
{[1, 2, 3].map((el, i) => (
<div
key={"element" + i}
ref={(element) => handleRefs(element, i)}
style={quickAndDirtyStyle}
>
<p>{pressedIndex === i ? "Dragging..." : "Press to drag"}</p>
</div>
))}
</>
);
};
export default function App() {
return (
<div className="App">
<DraggableComponent />
</div>
);
}
In useDraggable function, create an Array of refs :
const refs = useRef([]);
const unsubscribe = useRef();
const legacyRef = useCallback((elem, index) => {
refs.current[index] = elem;
if (unsubscribe.current) {
unsubscribe.current();
}
if (!elem) {
return;
}
in handleMouseMove use this array :
if (!refs.current || !position.current) {
return;
}
const pos = position.current;
const elem = refs.current[index];
position.current = onDrag({
[...]
The idea is to use the array index to assign the ref to each element.
jHope it helps
Instead of trying to declare an array of refs try rendering a component while iterating over the array and declare single ref in that component.

React useRef on Canvas getting null or undefined

I am trying to reference a canvas DOM element to make the width of it the same as its parent without messing with the scale of the canvas. When doing so I am getting a Uncaught TypeError: _canvasRef$current.getContext is not a function error and am not exactly sure how to fix it.
Ive console logged a few things just to make the app run but the issue is within the useEffect
import SignaturePad from 'react-signature-canvas'
const canvasRef = useRef<HTMLCanvasElement>(null)
useEffect(() => {
const ctx: CanvasRenderingContext2D | null | undefined =
canvasRef.current?.getContext('2d')
const ctxContainer = document.getElementById('ctxContainer')
console.log('ctx: ', ctx)
// if (ctx && ctxContainer) {
// ctx.style.width = '100%'
// ctx.style.height = '100%'
// ctx.width = ctxContainer.clientWidth
// }
}, [])
const save = () => {
console.log('saved!')
}
console.log('canvasRef: ', canvasRef)
return (
<div id={`ctxContainer`} className={'signatureInput__option-draw'}>
<SignaturePad
ref={canvasRef}
canvasProps={{
className: 'signatureInput__option-draw__canvas',
}}
onEnd={save}
/>
</div>
)
Try this.
Note that you may still need to do some changes to following code to full fill your full requirement ( such as event handling when the user change the screen sizes. ) The following code will fix the issue you are facing right now.
useEffect(() => {
if (canvasRef.current) {
const canvas = canvasRef.current.getCanvas();
const ctx = canvas.getContext('2d');
// do something with the canvas ref
if (ctx) {
ctx.canvas.width = window.innerHeight;
ctx.canvas.height = window.innerHeight;
}
}
}, []);
Try using getCanvas() . i dont think getContext() is provided with the package
function App() {
const canvasRef = React.createRef(null);
useEffect(() => {
const canvas = canvasRef.current;
console.log(canvas.current);
const ctx = canvas.getCanvas();
const ctxContainer = document.getElementById("ctxContainer");
console.log("ctx: ", ctx);
if (ctx && ctxContainer) {
ctx.style.width = "100%";
ctx.style.height = "100%";
ctx.width = ctxContainer.clientWidth;
}
}, []);
const save = () => {
console.log("saved!");
};
console.log("canvasRef: ", canvasRef);
return (
<div className="App">
<div id={`ctxContainer`} className={"signatureInput__option-draw"}>
<SignaturePad
ref={(ref) => {
canvasRef.current = ref;
}}
canvasProps={{
className: "signatureInput__option-draw__canvas",
}}
onEnd={save}
/>
</div>
</div>
);
}

ReactJS : eventListener tranistionend not getting cleaned up in useEffect

I have an image carousel component which has a smooth transition between images using the eventListener transtionend.
This event listener even though I have a cleanup function in place it creates a memory leak.
When I leave the page that has the image carousel the error does not appear yet. However, if I return to the page with the carousel and the transition completes one cycle (the image changes) then I get the error in the console.
Warning: Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in a
useEffect cleanup function.
I attached my code below:
/** #jsx jsx */
import { useState, useEffect, useRef } from "react";
import { css, jsx } from "#emotion/core";
import SliderContent from "./SliderContent";
import Slide from "./Slide";
import Arrow from "./Arrow";
import Dots from "./Dots";
export default function Slider({ autoPlay }) {
const getWidth = () => window.innerWidth * 0.8;
const slides = [
"https://images.unsplash.com/photo-1449034446853-66c86144b0ad?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2100&q=80",
"https://images.unsplash.com/photo-1470341223622-1019832be824?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2288&q=80",
"https://images.unsplash.com/photo-1448630360428-65456885c650?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2094&q=80",
"https://images.unsplash.com/photo-1534161308652-fdfcf10f62c4?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2174&q=80",
];
const firstSlide = slides[0];
const secondSlide = slides[1];
const lastSlide = slides[slides.length - 1];
const [isTabFocused, setIsTabFocused] = useState(true);
const [isButtonDisabled, setIsButtonDisabled] = useState(false);
const [state, setState] = useState({
translate: 0,
transition: 0.9,
activeSlide: 0,
_slides: [firstSlide, secondSlide, lastSlide],
});
const { activeSlide, translate, _slides, transition } = state;
const autoPlayRef = useRef();
const transitionRef = useRef();
const resizeRef = useRef();
const focusedTabRef = useRef();
const blurredTabRef = useRef();
useEffect(() => {
//eslint-disable-next-line react-hooks/exhaustive-deps
if (transition === 0) setState({ ...state, transition: 0.9 });
}, [transition]);
useEffect(() => {
transitionRef.current = smoothTransition;
resizeRef.current = handleResize;
focusedTabRef.current = handleFocus;
blurredTabRef.current = handleBlur;
autoPlayRef.current = handleAutoPlay;
});
useEffect(() => {
const play = () => autoPlayRef.current();
let interval = null;
if (autoPlay) {
interval = setInterval(play, autoPlay * 1000);
}
return () => {
if (autoPlay) {
clearInterval(interval);
}
};
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [isButtonDisabled, autoPlay]);
useEffect(() => {
const smooth = (e) => {
if (typeof e.target.className === "string" || e.target.className instanceof String) {
if (e.target.className.includes("SliderContent")) {
transitionRef.current();
}
}
};
const resize = () => resizeRef.current();
const onFocusAction = () => focusedTabRef.current();
const onBlurAction = () => blurredTabRef.current();
const transitionEnd = window.addEventListener("transitionend", smooth);
const onResize = window.addEventListener("resize", resize);
const onFocus = window.addEventListener("focus", onFocusAction);
const onBlur = window.addEventListener("blur", onBlurAction);
return () => {
window.removeEventListener("resize", onResize);
window.removeEventListener("focus", onFocus);
window.removeEventListener("blur", onBlur);
window.removeEventListener("transitionend", transitionEnd);
};
//eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (isButtonDisabled) {
const buttonTimeout = setTimeout(() => {
setIsButtonDisabled(false);
}, 1000);
return () => clearTimeout(buttonTimeout);
}
//eslint-disable-next-line react-hooks/exhaustive-deps
}, [isButtonDisabled]);
const handleFocus = () => setIsTabFocused(true);
const handleBlur = () => setIsTabFocused(false);
const handleAutoPlay = () => isTabFocused && nextSlide();
const handleResize = () => setState({ ...state, translate: getWidth(), transition: 0 });
const nextSlide = () => {
if (!isButtonDisabled) {
setState({
...state,
translate: translate + getWidth(),
activeSlide: activeSlide === slides.length - 1 ? 0 : activeSlide + 1,
});
}
setIsButtonDisabled(true);
};
const prevSlide = () => {
if (!isButtonDisabled) {
setState({
...state,
translate: 0,
activeSlide: activeSlide === 0 ? slides.length - 1 : activeSlide - 1,
});
}
setIsButtonDisabled(true);
};
const smoothTransition = () => {
let _slides = [];
// We're at the last slide.
if (activeSlide === slides.length - 1)
_slides = [slides[slides.length - 2], lastSlide, firstSlide];
// We're back at the first slide. Just reset to how it was on initial render
else if (activeSlide === 0) _slides = [lastSlide, firstSlide, secondSlide];
// Create an array of the previous last slide, and the next two slides that follow it.
else _slides = slides.slice(activeSlide - 1, activeSlide + 2);
setState({
...state,
_slides,
transition: 0,
translate: getWidth(),
});
};
return (
<div css={SliderCSS}>
<SliderContent
translate={translate}
transition={transition}
width={getWidth() * _slides.length}
>
{_slides.map((slide, i) => (
<Slide width={getWidth()} key={slide + i} content={slide} />
))}
</SliderContent>
<Arrow direction="left" handleClick={prevSlide} isDisabled={isButtonDisabled} />
<Arrow direction="right" handleClick={nextSlide} isDisabled={isButtonDisabled} />
<Dots slides={slides} activeIndex={activeSlide} />
</div>
);
}
const SliderCSS = css`
position: relative;
height: 600px;
width: 80%;
margin: 40px auto 0px auto;
overflow: hidden;
`;
The window listener is getting removed at the end of the useEffect but I don't know why it still creates the memory leak.
Hmm. It seems you're removing event listeners incorrectly. DOM addEventListener returns nothing (undefined).
Wrong:
const onResize = window.addEventListener("resize", resize);
window.removeEventListener("resize", onResize);
Should be:
window.addEventListener("resize", resize);
window.removeEventListener("resize", resize);

Categories

Resources