Make two references to the same React element [duplicate] - javascript

I'm using the useHover() react hook defined in this recipe. The hook returns a ref and a boolean indicating whether the user is currently hovering over element identified by this ref. It can be used like this...
function App() {
const [hoverRef, isHovered] = useHover();
return (
<div ref={hoverRef}>
{isHovered ? 'Hovering' : 'Not Hovering'}
</div>
);
}
Now let's say that I want to use another (hypothetical) hook called useDrag which returns a ref and a boolean indicating whether the user is dragging the current element around the page. I want to use this on the same element as before like this...
function App() {
const [hoverRef, isHovered] = useHover();
const [dragRef, isDragging] = useDrag();
return (
<div ref={[hoverRef, dragRef]}>
{isHovered ? 'Hovering' : 'Not Hovering'}
{isDragging ? 'Dragging' : 'Not Dragging'}
</div>
);
}
This won't work because the ref prop can only accept a single reference object, not a list like in the example above.
How can I approach this problem so I can use multiple hooks like this on the same element? I found a package that looks like it might be what I'm looking for, but I'm not sure if I'm missing something.

A simple way to go about this is documented below.
Note: the ref attribute on elements takes a function and this function is later called with the element or node when available.
function App() {
const myRef = useRef(null);
return (
<div ref={myRef}>
</div>
);
}
Hence, myRef above is a function with definition
function(element){
// something done here
}
So a simple solution is like below
function App() {
const myRef = useRef(null);
const anotherRef = useRef(null);
return (
<div ref={(el)=> {myRef(el); anotherRef(el);}}>
</div>
);
}

A React ref is really nothing but a container for some mutable data, stored as the current property. See the React docs for more details.
{
current: ... // my ref content
}
Considering this, you should be able to sort this out by hand:
function App() {
const myRef = useRef(null);
const [hoverRef, isHovered] = useHover();
const [dragRef, isDragging] = useDrag();
useEffect(function() {
hoverRef.current = myRef.current;
dragRef.current = myRef.current;
}, [myRef.current]);
return (
<div ref={myRef}>
{isHovered ? 'Hovering' : 'Not Hovering'}
{isDragging ? 'Dragging' : 'Not Dragging'}
</div>
);
}

Related

Pass props and use ID again

I have a component that passes props to another component. Inside the component the props have been passed to, I declare the parameter set new variable and get the last item of the array like this:
var lastItem = passedProp[passedProp - 1] || null
My question is how do I pass this property back to another component to use in a global service I am using to run inside a function. From what I am aware props can only be passed down in React, not up? Please correct me if I am wrong. The end result I want to achieve is to use this property's ID in function I am using in global service.
read about lifting state up ...
https://reactjs.org/tutorial/tutorial.html#lifting-state-up
You can pass a function to the child and the child can pass the information through this function.
I let you an example that you can copy & paste to see how it works :)
import React from 'react';
function ChildComponent(props) {
const { data, passElementFromChild } = props;
const lastElement = data[data.length - 1] || null;
setTimeout(() => {
passElementFromChild('this string is what the parent is gonna get');
}, 300);
return (
<div>Last element of the array is: {lastElement}</div>
);
}
function Question17() {
const data = ['firstElement', 'middleElement', 'lastElement']
const passElementFromChild = (infoFromChild) => {
console.log("infoFromChild: ", infoFromChild);
}
return (
<ChildComponent data={data} passElementFromChild={passElementFromChild} />
);
}
export default Question17;

Get total width of JSX Element

How could I get the width of a JSX.Element? If I was in vanilla, I would do something like
const button = document.createElement('button');
document.body.appendChild(button)
window.getComputedStyle(button).width
Now I need to do the same, but it seems ref is null and I'm not sure even how to temporarily append to the DOM just to see what its width would be.
const button: JSX.Element = <CustomButton/>;
/// ....
Couple of things to check and consider.
where is the ref created and does it get forwarded correctly to a
valid dom element (make sure that CustomButton uses forwardRef).
you don't need to append anything in react to look at the width. all you need is ref.current.clientWidth or ref.current.getBoundingClientRect(), but ref.current has to exist in the first place :-)
if you need access to the ref.current element when your component first mounts (and not in a onClick or some other callback - then this does not apply) you'll have to use useLayoutEffect as the javascript runs before the dom is rendered so there is technically no to measure yet.
See this example:
ParentComponent.tsx
import {useState, useLayoutEffect} from 'react';
const ParentComponent = () => {
const [width, setWidth] = useState<number | null>(null);
useLayoutEffect(() => {
if(ref?.current && !width) {
const { clientWidth } = ref.current;
setWidth(clientWidth);
}
}, [ref?.current]);
console.log('width', width);
// `width` will be null at first render,
// then when CustomButton renders and <button> is created the ref will be
// updated, triggering your layout side effect that saves the
// clientWidth to the state. State change will trigger a rerender of
// ParentComponent and your console.log will finally print the width (whose
// value is stored in the state).
return <CustomButton ref={ref}/>;
};
CustomButton.tsx
import {forwardRef} from 'react';
const CustomButton = forwarRef((props, ref) => {
return (
<>
// some other stuff
<button ref={ref}/>>Click</button>
</>
);
};
The canonical way is to use a ref, and then observe it within an effect, which is called after the DOM gets rendered:
const ref = useRef();
useEffect(() => {
console.log(window.getComputedStyle(ref.current).width)
}, []);
return <button ref={ref}/>;

How to make the component "wait" for state to update in react?

I'm learning react by making a battleship game. When the component loads, I create a new object (board) which I set as a state. Then I'd like to map the board itself, which is any array. However, react says
cannot read property board of undefined.
With console logging, I found out that at first when the page loads, playerBoard is an empty object, and only THEN sets it to the given object with setPlayerBoard.
How could I avoid this?
App.js looks like this:
const GameControl = () => {
const [playerBoard, setPlayerBoard] = useState({})
//creating the board object when component mounts, setting it as a state
useEffect(() => {
const playerBoard = GameBoard('player');
setPlayerBoard({...playerBoard});
},[])
return (
<div className = 'board-container'>
<div className = "board player-board">
{ //mapping an array
playerBoard.boardInfo.board.map((cell, i) => {
return (
<div className = {`cell player-cell`key = {i}></div>
)
} )
}
</div>
</div>
)
}
If creating the game board is synchronous, then just use that as your default value for the state:
const [playerBoard, setPlayerBoard] = useState(GameBoard('player'));
// No use effect after this
If creating the gameboard is synchronous but expensive, you can instead pass a function into useState, and that function will only be called on the first render:
const [playerBoard, setPlayerBoard] = useState(() => GameBoard('player'));
If creating the game board is asynchronous, then you're right to use an effect, but there is no way to avoid the first render not having data. You will just need to make sure your component can work when it doesn't have data. A common way to do this is to start with the state being an value that indicates no data, and then check for it and return a placeholder. null's easier to check for than an empty object, so i'd recommend that:
const [playerBoard, setPlayerBoard] = useState(null);
useEffect(() => {
// some asynchronous stuff, followed by a call to setPlayerBoard
},[])
if (!playerBoard) {
return <div>Loading...</div>
// Or if you don't want to show anything:
// return null;
}
return (
<div className='board-container'>
// etc
</div>
);

Unexpected behavior with useState hook React

I have some experience with class components in React, but am trying to learn hooks and functional components better.
I have the following code:
import React, { useState } from "react";
import { Button } from "reactstrap";
import StyleRow from "./StyleRow";
export default function Controls(props) {
const [styles, setStyles] = useState([]);
function removeStyle(index) {
let newStyles = styles;
newStyles.splice(index, 1);
setStyles(newStyles);
}
return (
<div>
{styles}
<Button
color="primary"
onClick={() => {
setStyles(styles.concat(
<div>
<StyleRow />
<Button onClick={() => removeStyle(styles.length)}>x</Button>
</div>
));
}}
>
+
</Button>
</div>
);
}
The goal of this code is to have an array of components that have an "x" button next to each one that removes that specific component, as well as a "+" button at the bottom that adds a new one. The StyleRow component just returns a paragraph JSX element right now.
The unusual behavior is that when I click the "x" button by a row, it removes the element and all elements following it. It seems that each new StyleRow component that is added takes the state of styles at the moment of its creation and modifies that instead of always modifying the current styles state. This is different behavior than I would expect from a class component.
The freezing of state leads me to believe this has something to do with closures, which I don't fully understand, and I am curious to know what here triggered them. If anyone knows how to solve this problem and always modify the same state, I would greatly appreciate it.
Finally, I think this post on SO is similar, but I believe it addresses a slightly different question. If someone can explain how that answer solves this problem, of course feel free to close this question. Thank you in advance!
You are modifying the existing state of styles, so you will need to create a deep copy of the array first.
You can either write your own clone function, or you can import the Lodash cloneDeep function.
Add the following dependency to your package.json using:
npm install lodash
Also, you are passing the length of the array to the removeStyle function. You should be passing the last index which is length - 1.
// ...
import { cloneDeep } from 'lodash';
// ...
function removeStyle(index) {
let newStyles = cloneDeep(styles); // Copy styles
newStyles.splice(index, 1); // Splice from copy
setStyles(newStyles); // Assign copy to styles
}
// ...
<Button onClick={() => removeStyle(styles.length - 1)}>x</Button>
// ...
If you want to use a different clone function or write your own, there is a performance benchmark here:
"What is the most efficient way to deep clone an object in JavaScript?"
I would also move the function assigned to the onClick event handler in the button outside of the render function. It looks like you are calling setStyles which adds a button with a removeStyle event which itself calls setStyles. Once you move it out, you may be able to better diagnose your issue.
Update
I rewrote your component below. Try to render elements using the map method.
import React, { useState } from "react";
import { Button } from "reactstrap";
const Controls = (props) => {
const [styles, setStyles] = useState([]);
const removeStyle = (index) => {
const newStyles = [...styles];
newStyles.splice(index, 1);
setStyles(newStyles);
};
const getChildNodeIndex = (elem) => {
let position = 0;
let curr = elem.previousSibling;
while (curr != null) {
if (curr.nodeType !== Node.TEXT_NODE) {
position++;
}
curr = curr.previousSibling;
}
return position;
};
const handleRemove = (e) => {
//removeStyle(parseInt(e.target.dataset.index, 10));
removeStyle(getChildNodeIndex(e.target.closest("div")));
};
const handleAdd = (e) => setStyles([...styles, styles.length]);
return (
<div>
{styles.map((style, index) => (
<div key={index}>
{style}
<Button data-index={index} onClick={handleRemove}>
×
</Button>
</div>
))}
<Button color="primary" onClick={handleAdd}>
+
</Button>
</div>
);
};
export default Controls;
I've added the most preferred way in a new answer as the previous one was becoming too long.
The explanation lies in my previous answer.
import React, { useState } from "react";
import { Button } from "reactstrap";
export default function Controls(props) {
const [styles, setStyles] = useState([]);
function removeStyle(index) {
let newStyles = [...styles]
newStyles.splice(index, 1);
setStyles(newStyles);
}
const addStyle = () => {
const newStyles = [...styles];
newStyles.push({content: 'ABC'});
setStyles(newStyles);
};
// we are mapping through styles and adding removeStyle newly and rerendering all the divs again every time the state updates with new styles.
// this always ensures that the removeStyle callback has reference to the latest state at all times.
return (
<div>
{styles.map((style, index) => {
return (
<div>
<p>{style.content} - Index: {index}</p>
<Button onClick={() => removeStyle(index)}>x</Button>
</div>
);
})}
<Button color="primary" onClick={addStyle}>+</Button>
</div>
);
}
Here is a CodeSandbox for you to play around.
Let's try to understand what's going on here.
<Button
color="primary"
onClick={() => {
setStyles(styles.concat(
<div>
<StyleRow />
<Button onClick={() => removeStyle(styles.length)}>x</Button>
</div>
));
}}
>
+
</Button>
First render:
// styles = []
You add a new style.
// styles = [<div1>]
The remove callback from the div is holding the reference to styles, whose length is now 0
You add one more style. // styles = [<div1>, <div2>]
Since div1 was created previously and didn't get created now, its still holding a reference to styles whose length is still 0.
div2 is now holding a reference to styles whose length is 1.
Now the same goes for the removeStyle callback that you have. Its a closure, which means it's holding a reference to a value of its outer function, even after the outer function has done executing. So when removeStyles is called for the first div1 the following lines will execute:
let newStyles = styles; // newStyles []
newStyles.splice(index, 1); // index(length) = 0;
// newStyles remain unchanged
setStyles(newStyles); // styles = [] (new state)
Now consider you have added 5 styles. So this is how the references will be held by each div
div1 // styles = [];
div2 // styles = [div1];
div3 // styles = [div1, div2];
div4 // styles = [div1, div2, div3];
div5 // styles = [div1, div2, div3, div4];
So what happens if you try to remove div3, the following removeStyly will execute:
let newStyles = styles; // newStyles = [div1, div2]
newStyles.splice(index, 1); // index(length) = 2;
// newStyles remain unchanged; newStyles = [div1, div2]
setStyles(newStyles); // styles = [div2, div2] (new state)
Hope that helps and addresses your concern. Feel free to drop any questions in the comments.
Here is a CodeSandbox for you to play around with and understand the issue properly.

Dynamically creating refs without strings

EDIT: We're using React 16.2.0, which is relevant to the question (see this answer).
So far as I can tell, this is the accepted way to create a ref (at least for our version of react):
<div ref={(r) => { this.theRef = r; }}>Hello!</div>
And then it can be used something like this:
componentDidMount() {
if (this.theRef) {
window.addEventListener('scroll', this.handleScroll);
}
}
This works fine. However, if I want to create a dynamically named ref, say as part of a loop, how do I go about naming the ref?
Put in now obsolete terms, I would like something along these lines:
<div ref="{refName}">Hello!</div>
Thanks!
Try just:
<div ref={refName}>Hello!</div>
For a map you need a key, so maybe you could just use that key to map to an object? Like so:
this.myRefs = {}
doSomethingToRef = (key) => {
this.myRefs[key].doSomething()
}
return (
myValues.map(value => (
<div key={value.key} ref = {r => {this.myRefs[value.key] = r}}>{...}</div>
))
)
Use ref like this:
Define the refName inside the class constructor:
this.refName = React.createRef()
Assign the ref in your element:
<div ref={this.refName} id="ref-name">Hello!</div>
To access the refName, use current:
this.refName.current
Example:
if (this.refName.current.id == 'ref-name') {
window.addEventListener('scroll', this.handleScroll);
}
Update
As per your comment, to use ref in older version, you may use just like this:
<div ref={(el) => this.refName = el} id="ref-name">Hello!</div>
{/* notice double quote is not used */}
To access:
this.refs.refName
Example:
if (this.refs.refName.id == 'ref-name') {
window.addEventListener('scroll', this.handleScroll);
}
To do it in more better way, you may use callback pattern.
[short-id][1] might be a good candidate!
It has methods like:
ids.store('foo'); // "8dbd46"
ids.fetch('8dbd46'); // 'foo'
ids.fetch('8dbd46'); // undefined
ids.configure(Object conf)
$ npm install short-id
RefsCmp.js
var shortid = require('shortid');
const RefsList = (newrefs) = > (
{newrefs.map(item => (
<div ref={shortid.generate()}>Hello!</div>
))}
)
export default RefsList;

Categories

Resources