useRef type on React Draggable - reactjs

I am relatively new to coding and especially to Typescript. I am trying to create a React Draggable modal that changes its bounds when the window gets resized and move the modal with it, so it never gets out of the window. I have created a function that does that but I am struggling to use the correct type for the ref draggableRef that is used on the actual draggable. What type is the draggableRef? Whatever I change it to there is problem with the .state.x; on the modalOffsetLeft that it doesn't exist on it.
Or, is there other way to do it? Thanks!
here is the code:
export const DraggableModal: React.FC<DraggableModalProps> = ({
draggableProps,
onClose,
}) => {
const [bounds, setBounds] = useState({
width: 0,
height: 0,
});
const ref = useRef<HTMLDivElement>(null);
const draggableRef = useRef<DraggableCore>(null);
const rootWindow = document.getElementById('root') as HTMLDivElement;
const getValues = () => {
if (ref.current !== null && draggableRef.current !== null) {
const modalWidth = ref?.current?.clientWidth;
const draggableWindowWidth = rootWindow?.clientWidth - modalWidth;
const modalOffsetLeft = draggableRef?.current?.state.x;
setBounds({
width: draggableWindowWidth,
height: rootWindow?.clientHeight - ref.current.offsetHeight,
});
if (modalOffsetLeft > draggableWindowWidth) {
draggableRef.current.state.x = draggableWindowWidth;
}
}
};
useEffect(() => {
getValues();
}, []);
parent.onresize = getValues;
return (
<Draggable
ref={draggableRef}
bounds={{
left: 0,
top: 0,
right: bounds.width,
bottom: bounds.height,
}}
{...draggableProps}
handle="#handle"
>
<DraggableWrapper>
...
</DraggableWrapper>
....

instead of updating its x,y using ref try to create a state {x,y} and then update this x and y.
then pass x and y to position props in Draggable.
by the way you can run the function get values in the more clearing way by adding the function to a listner that runs when the window is changes.

Related

React useEffect dependency or useCallback seem to cause re-renders

I have a useEffect function that takes an image, resizes it for display and makes it ready for upload. While this works I get the React warning to list the function in the useEffect dependency list. But when I do this it causes continuous re-renders even when moving the function to a useCallback. The code is as follows ...
import React, { useEffect, useRef, useCallback } from 'react'
const Canvas = (props) => {
const { width = 180, height = 220, img, onChange = null, setFile = null} = props
const canvas = useRef(null)
const setBlobFile = useCallback(
(blob) => {
setFile(blob)
},
[setFile]
)
useEffect(() => {
if (img) {
console.log("Canvas useEffect ...", img)
var image = new Image()
image.src = img
image.onload = () => {
canvas.current.getContext("2d")
canvas.current.getContext("2d").clearRect(0, 0, width, height);
canvas.current.getContext("2d").drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height)
canvas.current.toBlob((blob) => {
setBlobFile(blob)
})
}
}
}, [img, width, height, setBlobFile])
const onFileSelect = (e) => {
const objectURL = URL.createObjectURL(e.target.files[0])
onChange(objectURL)
}
return (
<div onClick={onClick}>
<label htmlFor='upload'>
<canvas ref={canvas} width={width} height={height} />
<input type='file' id='upload' onChange={(e) => onFileSelect(e)} style={{ display: 'none' }} />
</label>
</div>
)
}
export { Canvas as default }
Parent Component provides the setFile routine to set the file for upload
...
const [selectedFile, setSelectedFile] = useState(new File([""], ""))
const setFile = (aBlob) => {
var img = new Image()
img = aBlob
setSelectedFile(new File([aBlob], "image.png", {
type: 'image/png',
}))
}
...
Any ideas why I'm seeing this behaviour? I thought the useCallback() approach was supposed to stop the re-renders but it seems to create them. How can I avoid the warning but also stop the re-rendering behaviour? Thanks for taking a look.
You have to use useCallback where the function is created, not where it is used.
const setBlobFile = useCallback(
(blob) => {
setFile(blob)
},
[setFile]
)
in Canvas is useless for preventing rerenders when setFile changes because setBlobFile will still change when setFile changes, since setFile is passed as a dependency.
Instead you need to to use useCallback in the parent component where setFile is created in the first place:
const [selectedFile, setSelectedFile] = useState(new File([""], ""))
const setFile = useCallback((aBlob) => {
var img = new Image()
img = aBlob
setSelectedFile(new File([aBlob], "image.png", {
type: 'image/png',
}))
}, [setSelectedFile]);
This is will work as expected because useState guarantees that the setter function (setSelectedFile) will never change between renders and therefore setFile will never change between renders.

Assigning ref in react-konva

How do we assign ref to the stage or layer object. I am using react-konva. When I do console.log(stageE1), it says undefined.
useEffect(() => {
var stage = new Konva.Stage({
container: 'stage',
width: window.innerWidth,
height: window.innerHeight,
ref: stageEl, // not working
onMouseDown: (e) => {
// deselect when clicked on empty area
const clickedOnEmpty = e.target === e.target.getStage()
if (clickedOnEmpty) {
selectShape(null)
}
},
})
stage.container().style.border = '1px solid grey'
var layer = new Konva.Layer({
ref: layerEl, // not working
})
stage.add(layer)
}, [])
This is not react-konva usage. You are using Konva API directly. If you do this, you probably don't need to use refs. But if you really want:
var layer = new Konva.Layer();
// set ref
layerEl.current = layer;
If you use react-konva, then you should define components in React way:
import { Stage, Layer } from 'react-konva';
const App = () => {
const layerEl = React.useRef();
return <Stage><Layer ref={layerEl} /></Stage>;
};

React state always returning previous (or initial) state inside callback from fabricjs

The code below is my minimal issue reproduce component. It initializes fabric canvas, and handles "mode" state. Mode state determines whether canvas can be edited and a simple button controls that state.
The problem is that even if mode,setMode works correctly (meaning - components profiler shows correct state after button click, also text inside button shows correct state), the state returned from mode hook inside fabric event callback, still returns initial state.
I suppose that the problem is because of the function passed as callback to fabric event. It seems like the callback is "cached" somehow, so that inside that callback, all the states have initial values, or values that were in state before passing that callback.
How to make this work properly? I would like to have access to proper, current state inside fabric callback.
const [canvas, setCanvas] = useState<fabric.Canvas | null>(null);
const [mode, setMode] = useState("freerun");
const canvasRef = React.useRef<HTMLCanvasElement>(null);
const modes = ["freerun", "edit"];
React.useEffect(() => {
const canvas = new fabric.Canvas(canvasRef.current, {
height: 800,
width: 800,
backgroundColor: 'yellow'
});
canvas.on('mouse:down', function (this: typeof canvas, opt: fabric.IEvent) {
const evt = opt.e as any;
console.log("currentMode", mode) // Not UPDATING - even though components profiler shows that "mode" state is now "edit", it still returns initial state - "freerun".
if (mode === "edit") {
console.log("edit mode, allow to scroll, etc...");
}
});
setCanvas(canvas);
return () => canvas.dispose();
}, [canvasRef])
const setNextMode = () => {
const index = modes.findIndex(elem => elem === mode);
const nextIndex = index + 1;
if (nextIndex >= modes.length) {
setMode(modes[0])
} else {
setMode(modes[nextIndex]);
}
}
return (
<>
<div>
<button onClick={setNextMode}>Current mode: { mode }</button>
</div>
{`Current width: ${width}`}
<div id="fabric-canvas-wrapper">
<canvas ref={canvasRef} />
</div>
</>
)
The problem is that mode is read and it's value saved inside the callback during the callback's creation and, from there, never update again.
In order to solve this you have to add mode on the useEffect dependencies. In this way each time that mode changes React will run again the useEffect and the callback will receive the updated (and correct) value.
That's true! It worked now, thanks Marco.
Now to not run setCanvas on each mode change, I ended up creating another useEffect hook to hold attaching canvas events only. The final code looks similar to this:
const [canvas, setCanvas] = useState<fabric.Canvas | null>(null);
const [mode, setMode] = useState("freerun");
const canvasRef = React.useRef<HTMLCanvasElement>(null);
const modes = ["freerun", "edit"];
React.useEffect(() => {
if (!canvas) {
return;
}
// hook for attaching canvas events
fabric.Image.fromURL(gd, (img) => {
if (canvas) {
canvas.add(img)
disableImageEdition(img);
}
});
canvas.on('mouse:down', function (this: typeof canvas, opt: fabric.IEvent) {
const evt = opt.e as any;
console.log("currentMode", mode) // works correctly now
if (mode === "edit") {
console.log("edit mode, allow to scroll, etc...");
}
});
}, [canvas, mode])
React.useEffect(() => {
const canvas = new fabric.Canvas(canvasRef.current, {
height: 800,
width: 800,
backgroundColor: 'yellow'
});
setCanvas(canvas);
return () => canvas.dispose();
}, [canvasRef])
const setNextMode = () => {
const index = modes.findIndex(elem => elem === mode);
const nextIndex = index + 1;
if (nextIndex >= modes.length) {
setMode(modes[0])
} else {
setMode(modes[nextIndex]);
}
}
return (
<>
<div>
<button onClick={setNextMode}>Current mode: { mode }</button>
</div>
{`Current width: ${width}`}
<div id="fabric-canvas-wrapper">
<canvas ref={canvasRef} />
</div>
</>
)
I also wonder if there are more ways to solve that - is it possible to solve this using useCallback hook?

How to refactor React mousemove Class to functional Component?

I have a button which closes a navigation. This button follows the mouse. Everything is working, but I have a depricationwarning, which I wanna get rid of, but don't know exactly how. (I only know that useEffect is playing a certain role:
Here is the class:
import React from "react"
class NavigationCloseMouseButton extends React.Component {
static defaultProps = {
visible: true,
offsetX: 0,
offsetY: 0,
}
state = {
xPosition: 0,
yPosition: 0,
mouseMoved: false,
listenerActive: false,
}
componentDidMount() {
this.addListener()
}
componentDidUpdate() {
this.updateListener()
}
componentWillUnmount() {
this.removeListener()
}
getTooltipPosition = ({ clientX: xPosition, clientY: yPosition }) => {
this.setState({
xPosition,
yPosition,
mouseMoved: true,
})
}
addListener = () => {
window.addEventListener("mousemove", this.getTooltipPosition)
this.setState({ listenerActive: true })
}
removeListener = () => {
window.removeEventListener("mousemove", this.getTooltipPosition)
this.setState({ listenerActive: false })
}
updateListener = () => {
if (!this.state.listenerActive && this.props.visible) {
this.addListener()
}
if (this.state.listenerActive && !this.props.visible) {
this.removeListener()
}
}
render() {
return (
<div
onClick={this.props.toggleNavigation}
className="tooltip color-bg"
style={{
display:
this.props.visible && this.state.mouseMoved ? "block" : "none",
opacity: this.props.visible && this.state.mouseMoved ? "1" : "0",
top: this.state.yPosition + this.props.offsetY,
left: this.state.xPosition + this.props.offsetX,
}}
>
Close Menu
</div>
)
}
}
export default NavigationCloseMouseButton
And this is what I've so far, but results with errors:
ReferenceError: getTooltipPosition is not defined
import React, { useState, useEffect } from "react"
const NavigationCloseMouseButton = () => {
const defaults = {
visible: true,
offsetX: 0,
offsetY: 0,
}
const defaultState = {
xPosition: 0,
yPosition: 0,
mouseMoved: false,
listenerActive: false,
}
const [defaultProps, setDefaultProps] = useState(defaults)
const [state, setState] = useState(defaultState)
useEffect(() => {
// Update the document title using the browser API
addListener()
}, [])
getTooltipPosition = ({ clientX: xPosition, clientY: yPosition }) => {
setState({
xPosition,
yPosition,
mouseMoved: true,
})
}
addListener = () => {
window.addEventListener("mousemove", getTooltipPosition)
setState({ listenerActive: true })
}
removeListener = () => {
window.removeEventListener("mousemove", getTooltipPosition)
setState({ listenerActive: false })
}
updateListener = () => {
if (!state.listenerActive && props.visible) {
addListener()
}
if (state.listenerActive && !props.visible) {
removeListener()
}
}
return (
<div
onClick={props.toggleNavigation}
className="tooltip color-bg"
style={{
display: props.visible && state.mouseMoved ? "block" : "none",
opacity: props.visible && state.mouseMoved ? "1" : "0",
top: state.yPosition + props.offsetY,
left: state.xPosition + props.offsetX,
}}
>
Close Menu
</div>
)
}
export default NavigationCloseMouseButton
Setting Defaults
You can destructure individual props from the props object (the argument of the function component). While destructuring, you can use the = operator to set a default value for when this prop is not set.
const NavigationCloseMouseButton = ({ visible = true, offsetX = 0, offsetY = 0, toggleNavigation }) => {
Updating a Listener
I'm sure there a lots of great answers about this so I won't go into too much detail.
You want to handle adding and removing the listener from inside your useEffect. You should use a useEffect cleanup function for the final remove. We don't want to be adding and removing the same listener so we can memoize it with useCallback.
I'm not sure what you are trying to do with listenerActive. This could be a prop, but it also seems a bit redundant with visible. I don't know that we need this at all.
Calculating Offset
I also don't know that it makes sense to pass offsetX and offsetY as props. We need the mouse to be on top of the tooltip in order for it to be clickable. We can measure the tooltip div inside this component and deal with it that way.
// ref to DOM node for measuring
const divRef = useRef<HTMLDivElement>(null);
// can caluculate offset instead of passing in props
const offsetX = -.5 * (divRef.current?.offsetWidth || 0);
const offsetY = -.5 * (divRef.current?.offsetHeight || 0);
Animation
Setting the style property display as "block" or "none" makes it hard to do any sort of CSS transition. Instead, I recommend that you handle style switching by changing the className. You could still set display: block and display: none on those classes, but I am choosing to use transform: scale(0); instead.
Code
const NavigationCloseMouseButton = ({
visible = true,
toggleNavigation
}) => {
// state of the movement
const [state, setState] = useState({
xPosition: 0,
yPosition: 0,
mouseMoved: false
});
// memoized event listener
const getTooltipPosition = useCallback(
// plain event, not a React synthetic event
({ clientX: xPosition, clientY: yPosition }) => {
setState({
xPosition,
yPosition,
mouseMoved: true
});
},
[]
); // never re-creates
useEffect(() => {
// don't need to listen when it's not visible
if (visible) {
window.addEventListener("mousemove", getTooltipPosition);
} else {
window.removeEventListener("mousemove", getTooltipPosition);
}
// clean-up function to remove on unmount
return () => {
window.removeEventListener("mousemove", getTooltipPosition);
};
}, [visible, getTooltipPosition]); // re-run the effect if prop `visible` changes
// ref to DOM node for measuring
const divRef = useRef(null);
// can caluculate offset instead of passing in props
const offsetX = -.5 * (divRef.current?.offsetWidth || 0);
const offsetY = -.5 * (divRef.current?.offsetHeight || 0);
// don't show until after mouse is moved
const isVisible = visible && state.mouseMoved;
return (
<div
ref={divRef}
onClick={toggleNavigation}
// control most styling through className
className={`tooltip ${isVisible ? "tooltip-visible" : "tooltip-hidden"}`}
style={{
// need absolute position to use top and left
position: "absolute",
top: state.yPosition + offsetY,
left: state.xPosition + offsetX
}}
>
Close Menu
</div>
);
};
Other Uses
We can easily make this NavigationCloseMouseButton into a more flexible MovingTooltip by removing some of the hard-coded specifics.
Get the contents from props.children instead of always using "Close Menu"
Accept a className as a prop
Change the name of toggleNavigation to onClick
Code Sandbox Demo

Accessing values from an eventListener inside a React Hook with an empty dependancy array

I am attempting to use React hooks to run a canvas animation that depends on mouse position. I am using a custom hook for mouse position, and wrote another custom hook to animate the canvas.
Placing an empty dependancy array in the animation hook keeps it from unmounting and remounting the animation loop anytime the mouse moves, as is mentioned in this tutorial and suggested in a note in the React docs.
So the code below works, in that I can access coords inside the drawNow() function, but mounting and unmounting the animation loop every time the mouse moves does not seem like an acceptable way to do things.
How does one access event listeners inside React hooks that are purposely set to have no dependancies?
Here is the animation and the draw function....
const drawNow = (context,coords) => {
context.fillStyle = '#fff';
context.beginPath();
context.arc(coords.x,coords.y,50,0,2*Math.PI); // need coords here
context.fill();
}
export const Canvas = () => {
let ref = React.useRef();
// custom hook that returns mouse position
const coords = useMouseMove();
React.useEffect(() => {
let canvas = ref.current;
let context = canvas.getContext('2d');
const render = () => {
aId = requestAnimationFrame(render);
drawNow(context,coords); // requires current mouse coordinates
};
let aId = requestAnimationFrame(render);
return () => cancelAnimationFrame(aId);
}, [coords]); // dependancy array should be left blank so requestAnimationFrame mounts only once?
return (
<canvas ref={ref}/>
style={{
width: '100%',
height: '100%',
}}
);
};
Here is the custom hook for the mouse coordinates (references this useEventListener)
export const useMouseMove = () => {
function getCoords(clientX,clientY) {
return {
x: clientX || 0,
y: clientY || 0
};
}
const [coords, setCoords] = useState(getCoords);
useEventListener('mousemove', ({ clientX, clientY }) => {
setCoords(getCoords(clientX,clientY));
});
return coords;
};
Thanks, and looking forward to understanding more about hooks and event listeners.
Okay, I figured out my problem. The issue is the useMouseMove() hook was updating the coordinates with useState, when really I wanted to be using useRef, allowing me to imperatively "modify a child outside the typical data flow", as mentioned here.
First, I combined the useMouseMove() hook with the more general useEventListener so I didn't have to navigate unnecessary abstractions:
// a function that keeps track of mouse coordinates with useState()
export const useMouseMove = () => {
function getCoords(clientX,clientY) {
return {
x: clientX || 0,
y: clientY || 0
};
}
const [coords, setCoords] = useState(getCoords);
useEffect(
() => {
function handleMove(e) {
setCoords(getCoords(e.clientX,e.clientY));
}
global.addEventListener('mousemove', handleMove);
return () => {
global.removeEventListener('mousemove', handleMove);
};
}
);
return coords;
};
The next step was to "convert" the above function from useState() to useRef():
// a function that keeps track of mouse coordinates with useRef()
export const useMouseMove = () => {
function getCoords(clientX,clientY) {
return {
x: clientX || 0,
y: clientY || 0
};
}
const coords = useRef(getCoords); // ref not state!
useEffect(
() => {
function handleMove(e) {
coords.current = getCoords(e.clientX,e.clientY);
}
global.addEventListener('mousemove', handleMove);
return () => {
global.removeEventListener('mousemove', handleMove);
};
}
);
return coords;
};
Finally, I can access the mouse coordinates inside the animation loop while also keeping the dependancy array blank, preventing the animation component from remounting every time the mouse moves.
// the animation loop that mounts only once with mouse coordinates
export const Canvas = () => {
let ref = React.useRef();
// custom hook that returns mouse position
const coords = useMouseMove();
React.useEffect(() => {
let canvas = ref.current;
let context = canvas.getContext('2d');
const render = () => {
aId = requestAnimationFrame(render);
drawNow(context,coords.current); // mouse coordinates from useRef()
};
let aId = requestAnimationFrame(render);
return () => cancelAnimationFrame(aId);
}, []); // dependancy array is blank, the animation loop mounts only once
return (
<canvas ref={ref}/>
style={{
width: '100%',
height: '100%',
}}
);
};
Thanks to this escape hatch, I am able to create endless web fun without remounting the animation loop.

Resources