Correct way to pass useRef hook to ref property - reactjs

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

Related

How should I update individual items' className onClick in a list in a React functional component?

I'm new to React and I'm stuck trying to get this onClick function to work properly.
I have a component "Row" that contains a dynamic list of divs that it gets from a function and returns them:
export function Row({parentState, setParentState}) {
let divList = getDivList(parentState, setParentState);
return (
<div>
{divList}
</div>
)
}
Say parentState could just be:
[["Name", "info"],
["Name2", "info2"]]
The function returns a list of divs, each with their own className determined based on data in the parentState. Each one needs to be able to update its own info in parentState with an onClick function, which must in turn update the className so that the appearance of the div can change. My code so far seems to update the parentState properly (React Devtools shows the changes, at least when I navigate away from the component and then navigate back, for some reason), but won't update the className until a later event. Right now it looks like this:
export function getDivList(parentState, setParentState) {
//parentState is an array of two-element arrays
const divList = parentState.map((ele, i) => {
let divClass = "class" + ele[1];
return (
<div
key={ele, i}
className={divClass}
onClick={() => {
let newParentState =
JSON.parse(JSON.stringify(parentState);
newParentState[i][1] = "newInfo";
setParentState(newParentState);}}>
{ele[0]}
</div>
)
}
return divList;
}
I have tried to use useEffect, probably wrong, but no luck. How should I do this?
Since your Row component has parentState as a prop, I assume it is a direct child of this parent component that contains parentState. You are trying to access getDivList in Row component without passing it as a prop, it won't work if you write your code this way.
You could use the children prop provided by React that allow you to write a component with an opening and closing tag: <Component>...</Component>. Everything inside will be in the children. For your code it would looks like this :
import React from 'react';
import { render } from 'react-dom';
import './style.css';
const App = () => {
const [parentState, setParentState] = React.useState([
['I am a div', 'bg-red'],
['I am another div', 'bg-red'],
]);
React.useEffect(
() => console.log('render on ParentState changes'),
[parentState]
);
const getDivList = () => {
return parentState.map((ele, i) => {
return (
<div
key={(ele, i)}
className={ele[1]}
onClick={() => {
// Copy of your state with the spread operator (...)
let newParentState = [...parentState];
// We don't know the new value here, I just invented it for the example
newParentState[i][1] = [newParentState[i][1], 'bg-blue'];
setParentState(newParentState);
}}
>
{ele[0]}
</div>
);
});
};
return <Row>{getDivList()}</Row>;
};
const Row = ({ children }) => {
return <>{children}</>;
};
render(<App />, document.getElementById('root'));
And a bit of css for the example :
.bg-red {
background-color: darkred;
color: white;
}
.bg-blue {
background-color:aliceblue;
}
Here is a repro on StackBlitz so you can play with it.
I assumed the shape of the parentState, yu will have to adapt by your needs but it should be something like that.
Now, if your data needs to be shared across multiple components, I highly recommand using a context. Here is my answer to another post where you'll find a simple example on how to implement a context Api.

React ref that depends by an element's reference does not get passed to the child components

The following code creates an object ref that's called editor, but as you see it depends by the contentDiv element that's a ref to a HTMLElement. After the editor object is created it needs to be passed to the TabularRibbon. The problem is that the editor is always null in tabular component. Even if I add a conditional contentDiv?.current, in front of this, it still remains null...
Anyone has any idea?
export const Editor = () => {
let contentDiv = useRef<HTMLDivElement>(null);
let editor = useRef<Editor>();
useEffect(() => {
let options: EditorOptions = { };
editor.current = new Editor(contentDiv.current, options);
return () => {
editor.current.dispose();
}
}, [contentDiv?.current])
return (
<div >
<TabularRibbon
editor={editor.current}
/>
<div ref={contentDiv} />
..........

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

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.

useState not setting after initial setting

I have a functional component that is using useState and uses the #react-google-maps/api component. I have a map that uses an onLoad function to initalize a custom control on the map (with a click event). I then set state within this click event. It works the first time, but every time after that doesn't toggle the value.
Function component:
import React, { useCallback } from 'react';
import { GoogleMap, LoadScript } from '#react-google-maps/api';
export default function MyMap(props) {
const [radiusDrawMode, setRadiusDrawMode] = React.useState(false);
const toggleRadiusDrawMode = useCallback((map) => {
map.setOptions({ draggableCursor: (!radiusDrawMode) ? 'crosshair' : 'grab' });
setRadiusDrawMode(!radiusDrawMode);
}, [setRadiusDrawMode, radiusDrawMode]); // Tried different dependencies.. nothing worked
const mapInit = useCallback((map) => {
var radiusDiv = document.createElement('div');
radiusDiv.index = 1;
var radiusButton = document.createElement('div');
radiusDiv.appendChild(radiusButton);
var radiusText = document.createElement('div');
radiusText.innerHTML = 'Radius';
radiusButton.appendChild(radiusText);
radiusButton.addEventListener('click', () => toggleRadiusDrawMode(map));
map.controls[window.google.maps.ControlPosition.RIGHT_TOP].push(radiusDiv);
}, [toggleRadiusDrawMode, radiusDrawMode, setRadiusDrawMode]); // Tried different dependencies.. nothing worked
return (
<LoadScript id="script-loader" googleMapsApiKey="GOOGLE_API_KEY">
<div className="google-map">
<GoogleMap id='google-map'
onLoad={(map) => mapInit(map)}>
</GoogleMap>
</div>
</LoadScript>
);
}
The first time the user presses the button on the map, it setss the radiusDrawMode to true and sets the correct cursor for the map (crosshair). Every click of the button after does not update radiusDrawMode and it stays in the true state.
I appreciate any help.
My guess is that it's a cache issue with useCallback. Try removing the useCallbacks to test without that optimization. If it works, you'll know for sure, and then you can double check what should be memoized and what maybe should not be.
I'd start by removing the one from toggleRadiusDrawMode:
const toggleRadiusDrawMode = map => {
map.setOptions({ draggableCursor: (!radiusDrawMode) ? 'crosshair' : 'grab' });
setRadiusDrawMode(!radiusDrawMode);
};
Also, can you access the state of the map options (the ones that you're setting with map.setOptions)? If so, it might be worth using the actual state of the map's option rather than creating your own internal state to track the same thing. Something like (I'm not positive that it would be map.options):
const toggleRadiusDrawMode = map => {
const { draggableCursor } = map.options;
map.setOptions({
draggableCursor: draggableCursor === 'grab' ? 'crosshair' : 'grab'
});
};
Also, I doubt this is the issue, but it looks like you're missing a closing bracket on the <GoogleMap> element? (Also, you might not need to create the intermediary function between onLoad and mapInit, and can probably pass mapInit directly to the onLoad.)
<GoogleMap id='google-map'
onLoad={mapInit}>
This is the solution I ended up using to solve this problem.
I basically had to switch out using a useState(false) for setRef(false). Then set up a useEffect to listen to changes on the ref, and in the actual toggleRadiusDraw I set the reference value which fires the useEffect to set the actual ref value.
import React, { useCallback, useRef } from 'react';
import { GoogleMap, LoadScript } from '#react-google-maps/api';
export default function MyMap(props) {
const radiusDrawMode = useRef(false);
let currentRadiusDrawMode = radiusDrawMode.current;
useEffect(() => {
radiusDrawMode.current = !radiusDrawMode;
});
const toggleRadiusDrawMode = (map) => {
map.setOptions({ draggableCursor: (!currentRadiusDrawMode) ? 'crosshair' : 'grab' });
currentRadiusDrawMode = !currentRadiusDrawMode;
};
const mapInit = (map) => {
var radiusDiv = document.createElement('div');
radiusDiv.index = 1;
var radiusButton = document.createElement('div');
radiusDiv.appendChild(radiusButton);
var radiusText = document.createElement('div');
radiusText.innerHTML = 'Radius';
radiusButton.appendChild(radiusText);
radiusButton.addEventListener('click', () => toggleRadiusDrawMode(map));
map.controls[window.google.maps.ControlPosition.RIGHT_TOP].push(radiusDiv);
});
return (
<LoadScript id="script-loader" googleMapsApiKey="GOOGLE_API_KEY">
<div className="google-map">
<GoogleMap id='google-map'
onLoad={(map) => mapInit(map)}>
</GoogleMap>
</div>
</LoadScript>
);
}
Not sure if this is the best way to handle this, but hope it helps someone else in the future.

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