Why calling useState update function through other function passed as prop to child component won't work? - reactjs

This is the parent Component.
export default function CanvasPanel(props) {
const [sketchCoordinates, setSketchCoordinates] = useState([]);
useEffect(() => {
console.log(sketchCoordinates);
}, [sketchCoordinates]);
const onAddPoint = function (mousePos) {
setSketchCoordinates([
...sketchCoordinates,
{ x: mousePos.x, y: mousePos.y }
]);
};
const onPrintObject = (toPrint) => {
console.log(toPrint);
};
return (
<div className="canvas-panel">
Points:
<hr />
{sketchCoordinates.join(", ")}
<Canvas
sketchCurveCoordinates={sketchCoordinates}
addPoint={onAddPoint}
printObject={onPrintObject}
/>
<Sidepanel createPoint={onAddPoint} />
</div>
);
}
This is the child Component
export default function Canvas(props) {
const getMousePos = function (event) {
const offset = canvasRef.current.getBoundingClientRect();
return {
x: event.clientX - offset.left,
y: event.clientY - offset.top
};
};
const handleOnMouseUp = (event) => {
const mousePos = getMousePos(event);
props.printObject(mousePos);
props.addPoint(mousePos);
};
return (
<div className="canvas-container">
<PureCanvas
handleOnMouseUp={handleOnMouseUp}
/>
</div>
);
};
This is the PureCanvas component
export default class PureCanvas extends Component {
shouldComponentUpdate() {
return false;
}
render() {
return (
<canvas
className="canvas"
width="750"
height="500"
onMouseUp={this.props.handleOnMouseUp}
></canvas>
);
}
}
The code is not complete as I removed all canvas related stuff because it doesn't really matter, but it has enough to explain and reproduce the behaviour. The addPoint function should append an object with x and y properties to the sketchCoordinates array through spread operator and the update function from useState. This works when calling addPoint({x: (any int), y: ({any int}) at the CanvasPanel component, but when calling at the Canvas component through props it works only once. The array appends the first element but then it just replaces that same element with a new one, when it should append that element. So, I know I could do this with classes but I can't understand why it won't work with hooks. The following image shows the console after some updates to de sketchCoordinates array:
Console
EDIT
Please consider this code sandbox for further enlightenment.

This doesn't appear to be an issue with anything specific to the hook: if we replace the <canvas> with a button that just calls handleOnMouseUp on click, it works as expected.
I suspect it's not anything to do with the hook, and perhaps it's some detail that's been simplified out of your example:
Here's a code sandbox with this simplified version, that replaces the canvas with just a button.

Related

Observe (get sized) control (listen to events) over a nested component in the react and typescript application via the forwardRef function

I have a functional component called MyDivBlock
const MyDivBlock: FC<BoxProps> = ({ }) => {
{getting data...}
return (
<>
<div className='divBlock'>
{data.map((todo: { id: string; title: string }) =>
<div key={todo.id}>{todo.id} {todo.title} </div>)}
</div>
</>
);
};
I use it in such a way that MyDivBlock is nested as a child of
const App: NextPage = () => {
return (
<div>
<Box >
<MyDivBlock key="key0" areaText="DIV1" another="another"/>
</Box>
</div>
)
}
Note that MyDivBlock is nested in Box and MyDivBlock has no ref attribute. This is important because I need to write Box code with no additional requirements for my nested children. And anyone who will use my Box should not think about constraints and ref attributes.
Then I need to get the dimensions of MyDivBlock in the code of Box component, and later attach some event listeners to it, such as scrolling. These dimensions and listeners will be used in the Box component. I wanted to use Ref to control it. That is, the Box will later observe changes in the dimensions and events of MyDivBlock by creating a ref-reference to them
I know that this kind of parent-child relationship architecture is implemented through forwardRef
And here is the Box code:
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
export interface BoxProps extends React.ComponentProps<any> {
children?: Element[];
className: string;
}
export const Box: React.FC<BoxProps> = ({ children, ...rest }: BoxProps): JSX.Element => {
const childRef = useRef<HTMLDivElement>();
const ChildWithForwardRef = forwardRef<HTMLDivElement>((props, _ref) => {
const methods = {
show() {
if (childRef.current) {
console.log("childRef.current is present...");
React.Children.forEach(children, function (item) {
console.log(item)})
console.log("offsetWidth = " + childRef.current.offsetWidth);
} else {
console.log("childRef.current is UNDEFINED");
}
},
};
useImperativeHandle(_ref, () => (methods));
return <div ref={childRef}> {children} </div>
});
ChildWithForwardRef.displayName = 'ChildWithForwardRef';
return (
<div
className={'BoxArea'}>
<button name="ChildComp" onClick={() => childRef.current.show()}>get Width</button>
<ChildWithForwardRef ref={childRef} />
</div>
);
}
export default Box;
The result of pressing the button:
childRef.current is present...
[...]
$$typeof: Symbol(react.element) key: "key0" props: {areaText: 'DIV1', another: 'another'}
[...] Object
offsetWidth = undefined
As you can see from the output, the component is visible through the created ref. I can even make several nested ones and get the same for all of them.
But the problem is that I don't have access to the offsetWidth and other properties.
The other challenge is how can I add the addEventListener?
Because it works in pure Javascript with their objects like Element, Document, Window or any other object that supports events, and I have ReactChildren objects.
Plus I'm using NextJS and TypeScript.
Didn't dive too deep into the problem, but this may be because you are passing the same childRef to both div inside ChildWithForwardRef and to ChildWithForwardRef itself. The latter overwrites the former, so you have the method .show from useImperativeHandle available but not offsetWidth. A quick fix is to rewrite ChildWithForwardRef to use its own ref:
const ChildWithForwardRef = forwardRef<HTMLDivElement>((props, _ref) => {
const ref = useRef<HTMLDivElement>()
const methods = {
show() {
if (ref.current) {
console.log("ref.current is present...");
React.Children.forEach(children, (item) => console.log(item))
console.log("offsetWidth = " + ref.current.offsetWidth);
} else {
console.log("ref.current is UNDEFINED");
}
},
};
useImperativeHandle(_ref, () => (methods));
// Here ref instead of childRef
return <div ref={ref}> {children} </div>
});
But really I don't quite get why you would need ChildWithForwardRef at all. The code is basically equivalent to this simpler version:
const Box: React.FC<BoxProps> = ({ children, ...rest }: BoxProps): JSX.Element => {
const childRef = useRef<HTMLDivElement>();
const showWidth = () => {
if(childRef.current) {
console.log("childRef.current is present...");
React.Children.forEach(children, item => console.log(item))
console.log("offsetWidth = " + childRef.current.offsetWidth);
} else {
console.log("childRef.current is UNDEFINED");
}
}
return (
<div className={'BoxArea'}>
<button name="ChildComp" onClick={showWidth}>get Width</button>
<div ref={childRef}>{children}</div>
</div>
);
}
You can't solve this completely with React. I solved it by wrapping the child component, making it take the form of the parent.

How can I reset a dragged component to its original position with react-draggable?

I try to implement a function in my app that allows the user to reset all the components that he dragged around to be reset to their original position.
I assume that this functionality exists in react-draggable because of this closed and released issue: "Allow reset of dragging position" (https://github.com/idanen/react-draggable/issues/7). However I did not find any hint in the documentation (https://www.npmjs.com/package/react-draggable).
There was one question with the same content in stackoverflow, but it has been removed (https://stackoverflow.com/questions/61593112/how-to-reset-to-default-position-react-draggable).
Thanks for your help :-)
The referenced issue on the GitHub references a commit. After taking a look at the changes made in this commit, I found a resetState callback added to the useDraggable hook. In another place in the commit, I found a change to the test file which shows usage of the hook.
function Consumer(props) {
const {
targetRef,
handleRef,
getTargetProps,
resetState,
delta,
dragging
} = useDraggable(props);
const { style = defaultStyle } = props;
return (
<main
className='container'
ref={targetRef}
data-testid='main'
style={style}
{...getTargetProps()}
>
{dragging && <span>Dragging to:</span>}
<output>
{delta.x}, {delta.y}
</output>
<button className='handle' ref={handleRef}>
handle
</button>
<button onClick={resetState}>reset</button>
</main>
);
}
The hook returns a set of callbacks, including this callback, which can be used to reset the state of the draggable.
I wanted the component to reset back to its original position when the component was dropped.
Using hooks I monitored if the component was being dragged and when it was false reset the position otherwise it would be undefined.
export default function DraggableComponent(props: any) {
const {label} = props
const [isDragging, setIsDragging] = useState<boolean>(false)
const handleStart = (event: any, info: DraggableData) => {
setIsDragging(true)
}
const handleStop = (event: any, info: DraggableData) => {
setIsDragging(false)
}
return (
<Draggable
onStart={handleStart}
onStop={handleStop}
position={!isDragging? { x: 0, y: 0 } : undefined}
>
<Item>
{label}
</Item>
</Draggable>
)
}
Simple approach would be:
creating a new component to wrap our functionality around the Draggable callbacks
reset position when onStop callback is triggered
Example:
import { useState } from 'react';
import Draggable, { DraggableData, DraggableEvent, DraggableProps } from 'react-draggable';
export function Drag({ children, onStop, ...rest }: Partial<DraggableProps>) {
const initial = { x: 0, y: 0 }
const [pos, setPos] = useState(initial)
function _onStop(e: DraggableEvent, data: DraggableData){
setPos(initial)
onStop?.(e, data)
}
return (
<Draggable position={pos} onStop={_onStop} {...rest}>
{children}
</Draggable>
)
}
Usage:
export function App() {
return (
<Drag> Drag me </Drag>
)
}
Note that this answer does not work.
None of these approaches worked for me, but tobi2424's post on issue 214 of the Draggable repo did. Here's a minimal proof-of-concept:
import React from "react";
import Draggable from "react-draggable";
const DragComponent = () => {
// Updates the drag position parameter passed to Draggable
const [dragPosition, setDragPosition] = React.useState(null);
// Fires when the user stops dragging the element
const choiceHandler = () => {
setDragPosition({x: 0, y: 0});
};
return (
<Draggable
onStop={choiceHandler}
position={dragPosition}
>
Drag me
</Draggable>
);
};
export default DragComponent;
Edit
The code above works intermittently but not particularly well. As far as I can work out, react-draggable stores data about the position of the dragged element somewhere outside of React, in order to preserve the position of the element between component refreshes. I was unable to determine how to reset the position of the element on command and none of the other example code solves the problem for me.
You can do this in a very haphazard manner. There may be another way to set state more safely on this but I didn't look too deeply into it.
import React from 'react';
export default class 😊 extends Component {
constructor(props) {
super(props);
this.draggableEntity = React.createRef();
}
resetDraggable() {
try {
this.draggableEntity.current.state.x = 0;
this.draggableEntity.current.state.y = 0;
} catch (err) {
// Fail silently
}
}
render() {
return (
<Draggable
ref={this.draggableEntity}
>
<img onClick={(e) => {this.resetDraggable()}}></img>
</Draggable>
)
}
}
There happens to be another way! You can use it's exposed ref element to reset its offset. This can be achieved like so:
import React, {useRef, useCallback} from "react";
import Draggable from "react-draggable";
const DragComponent = () => {
// Updates the drag position parameter passed to Draggable
const [dragPosition, setDragPosition] = React.useState(null);
const draggerRef = useRef(null);
// Fires when the user stops dragging the element
const resetDrag = useCallback(() => {
setDragPosition({x: 0, y: 0});
draggerRef.current?.setState({ x: 0, y: 0 }); // This is what resets it!
}, [setDragPosition, draggerRef]);
return (
<Draggable
ref={draggerRef}
onStop={resetDrag}
position={dragPosition}
>
Drag me
</Draggable>
);
};
export default DragComponent;

ReactJS sending ref to global useContext state (Konva)

I am using useContext as a global state solution. I have a Store.jsx which contains my state, and a reducer.jsx which reduces. I am using Konva to create some shapes on an HTML5 Canvas. My goal is when I click on a shape I want to update my global state with a reference to what is active, and when I click again, to clear the reference.
My Full Code can be found here:
https://codesandbox.io/s/staging-platform-2li83?file=/src/App.jsx
Problem:
The problem is when I update the global state via the onClick event of a shape, its says that the reference is 'null', but when I console.log the reference in the onClick I can see the correct reference.
I think I am missing an important point to how useRef works.
This is how the flow appears in my head when I think about this:
I create a canvas, and I map an array of rectangle properties. This creates 4 rectangles. I use a wrapper component that returns a rectangle.
{rectArray.map((rectangle, index) => {
return (
<RectWrapper key={index} rectangle={rectangle} index={index} />
);
})}
Inside the RectWrapper, I create a reference, pass it to the ref prop of the Rect. In the onclick function, when I console log 'shapeRef' I see the refence ONLY when dispatch is commented out. If I uncomment dispatch then it shows as null, and if I console log the state, the reference is always null.
const RectWrapper = ({ rectangle, index }) => {
const shapeRef = React.useRef();
return (
<Rect
x={rectangle.x + index * 100}
y={5}
width={50}
height={50}
fill="red"
ref={shapeRef}
onClick={() => {
console.log("ShapeRef: ");
console.log(shapeRef); // This correctly identifies the rect only when dispatch is uncommented
dispatch({
type: "active_image",
payload: {
index: index,
reference: shapeRef
}
});
}}
/>
);
};
perhaps I am going about this to wrong way with hooks. I am just trying to keep a global state of whats been clicked on because components in another file would rely on this state.
The problem is happening because you are creating RectWrapper component as a functional component within your App component causing a new reference of the component to be created again and again and thus the reference is lost
Move your RectWrapper into a separate component declared outside of App component and pass on dispatch as a prop to it
import React, { useEffect, useContext, useState, Component } from "react";
import { Stage, Layer, Rect, Transformer } from "react-konva";
import { Context } from "./Store.jsx";
import "./styles.css";
const RectWrapper = ({ rectangle, index, dispatch }) => {
const shapeRef = React.useRef();
return (
<Rect
x={rectangle.x + index * 100}
y={5}
width={50}
height={50}
fill="red"
ref={shapeRef}
onClick={() => {
console.log("ShapeRef: ");
console.log(shapeRef);
dispatch({
type: "active_image",
payload: {
index: index,
reference: shapeRef
}
});
}}
/>
);
};
export default function App() {
const [state, dispatch] = useContext(Context);
console.log("Global State:");
console.log(state);
const rectArray = [
{ x: 10, y: 10 },
{ x: 10, y: 10 },
{ x: 10, y: 10 },
{ x: 10, y: 10 }
];
return (
<div className="App">
<Stage height={500} width={500}>
<Layer>
{rectArray.map((rectangle, index) => {
return (
<RectWrapper
dispatch={dispatch}
key={index}
rectangle={rectangle}
index={index}
/>
);
})}
</Layer>
</Stage>
</div>
);
}
Working demo
I don't think you need to create a ref in RectWrapper, because onClick has one event parameter. And the ref of the element that was clicked can be found in the event:
onClick={(e) => {
const thisRef = e.target;
console.log(thisRef );
...
Here is a working version without useRef: https://codesandbox.io/s/peaceful-brook-je8qo

Correct way to pass useRef hook to ref property

I'm not sure how to formulate the question less vaguely, but it's about pass-by-value and pass-by-reference cases in react. And Hooks.
I am using gsap to animate a div slide-in and out is the context for this, but I'm going to guess that what the ref is used for shouldn't matter.
So, this works fine, even though this is a more class-component-typical way of passing a ref as i understand it:
const RootNavigation = () => {
var navbar = useRef();
const myTween = new TimelineLite({ paused: true });
const animate = () => {
myTween.to(navbar, 0.07, { x: "100" }).play();
};
return(
<div className="nav-main" ref={div => (navbar = div)}> // <<<<<<<<<< pass as a callback
...
</div>
)}
And this elicits a "TypeError: Cannot add property _gsap, object is not extensible" error, even though this is how the React Hooks guide would have me do it:
const RootNavigation = () => {
var navbar = useRef();
const myTween = new TimelineLite({ paused: true });
const animate = () => {
myTween.to(navbar, 0.07, { x: "100" }).play();
};
return(
<div className="nav-main" ref={navbar}> //<<<<<<<<<<<<< not passing a callback
...
</div>
)}
Could somebody explain to me what's going on here or even toss a boy a link to where it's already been explained? I'm sure some sort of Dan character has written about it somewhere, i'm just not sure what to google. Thank you!
In the first example you aren't using a ref, you are reassigning navbar through the ref callback so navbar is the DOM element.
It's the same as
let navbar = null;
return <div ref={node => (navbar = node)} />
In the second example you are using the ref object which is an object with a current property that holds the DOM element
const navbar = useRef(null)
return <div ref={navbar} />
navbar is now
{ current: the DOM element }
So you are passing the object into myTween.to() instead of the DOM element inside navbar.current
Now in the second example gsap is trying to extend the ref object itself and not the DOM element.
Why do we get the TypeError: Cannot add property _gsap, object is not extensible`?
If you look at the source code of useRef you will see on line 891
if (__DEV__) {
Object.seal(ref);
}
that React is sealing the ref object and JavaScript will throw an error when we try to extend it using Object.defineProperty() which is probably what gsap is doing.
The solution for using a ref will be to pass ref.current into tween.to()
const RootNavigation = () => {
const navbar = useRef()
const myTween = new TimelineLite({ paused: true });
const animate = () => {
myTween.to(navbar.current, 0.07, { x: "100" }).play()
}
return (
<div className="nav-main" ref={navbar}>
...
</div>
)
}

React Hooks UseRef issue

I'm trying to use React hooks. I have a problem with this code:
class VideoItem extends Component {
handlePlayingStatus = () => {
this.seekToPoint();
...
}
seekToPoint = () => {
this.player.seekTo(30); // this worked - seek to f.ex. 30s
}
render() {
const { playingStatus, videoId } = this.state;
return (
<Fragment>
<ReactPlayer
ref={player => { this.player = player; }}
url="https://player.vimeo.com/video/318298217"
/>
<button onClick={this.handlePlayingStatus}>Seek to</button>
</Fragment>
);
}
}
So I want to get ref from the player and use a seek function on that. This works just fine but I have a problem to change it to hooks code.
const VideoItem = () => {
const player = useRef();
const handlePlayingStatus = () => {
seekToPoint();
...
}
const seekToPoint = () => {
player.seekTo(30); // this does not work
}
return (
<Fragment>
<ReactPlayer
ref={player}
url="https://player.vimeo.com/video/318298217"
/>
<button onClick={handlePlayingStatus}>Seek to</button>
</Fragment>
);
}
How can I remodel it?
From the documentation:
useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component.
Thus your code should be:
player.current.seekTo(30);
(optionally check whether player.current is set)
useCallback might also be interesting to you.
useRef is a hook function that gets assigned to a variable, inputRef, and then attached to an attribute called ref inside the HTML element you want to reference.
Pretty easy right?
React will then give you an object with a property called current.
The value of current is an object that represents the DOM node you’ve selected to reference.
So after using useRef player contains current object, and inside current object, your all methods from vimeo player would be available(in your case seekTo(number))
so you should be using player.current.seekTo(30) instead.
refer this to know more about useRef.

Resources