Function passed to element ref in React never runs - javascript

I'm building an array of React symbols, and passing a function to ref which never runs, for some reason. I believe I'm following the documentation on how to do this. By the time this is done rendering this.divElement should be set to the native DOM node of the last element, but since the function never runs, it remains null.
divElement = null;
render() {
const activeKeys = ["test1", "test2", "test3"];
const infoDivs = activeKeys.map((key, i) => {
console.log(key, i);
return (
<div
className="test"
ref={ (divElement) => { alert('This never runs?'); this.divElement = divElement } }
>
Some div text.
</divs>
)
});
return (
<span className="info-container container">
{ this.infoDivs }
</span>
)
}
I know the loop is running, as it logs three times as expected. What am I missing?

Apparently you meant to use {infoDivs}, not { this.infoDivs }. Your infoDivs variable that returns your elements, is just a variable inside render, not a class field.
Elements from infoDivs are never rendered, so the divElement remains undefined.
return (
<span className="info-container container">
{infoDivs}
</span>
)

Related

Make two references to the same React element [duplicate]

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

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

Variable in event of react list component

Here is my code:
export default function App() {
const array = ["item1", "item2", "item3"];
let progress = "";
const list = array.map((item) => {
progress += `/${item}`;
return (
<input
key={item}
type="button"
value={item}
onClick={() => {
console.log(progress);
}}
/>
);
});
return <div className="App">{list}</div>;
}
Also, you can try it on sandbox
When I click item1 button on the page. Why the console log /item1/item2/item3?
The expected behavior is that:
When item1 button is clicked. Console log /item1;
When item2 button is clicked. Console log /item1/item2;
...
Thanks.
When you click the button, the arrow function you provided will fire. This means that the function will read the variable only when the arrow function is being called. And since the progress variable isn't being created inside the mapping or arrow function, but rather outside it, the last mapped input element will set it to item1item2item3, meaning it will be the same when you click each of the inputs.
To fix this, you could assign a local variable to be equal to progress inside the map method, like so:
export default function App() {
const array = ["item1", "item2", "item3"];
let progress = "";
const list = array.map((item) => {
progress += item;
let local = progress;
return (
<input
key={item}
type="button"
value={item}
onClick={() => {
console.log(local);
}}
/>
);
});
return <div className="App">{list}</div>;
}
I haven't tested this exactly, but it should work.
When your code is executed, the array.map method runs the callback function provided for every element of the array. So when your program finished executing, the value of progress will be item1item2item3 and all you're click handlers are closing over the same variable progress(In closures a reference to the variable is kept) and hence the result item1item2item3.
You can solve this by defining a new scope for every handler using the IIFE.
export default function App() {
const array = ["item1", "item2", "item3"];
var progress = "";
const list = array.map(function (item) {
progress += item;
return (
<input
key={item}
type="button"
value={item}
onClick={(function(progress){
return function () {
console.log(progress);
}
})(progress)}
/>
);
});
return <div className="App">{list}</div>;
}```

How to call a function with different values without recreating an new arrow function on every render?

It's clear and we all know that using arrow functions in the render method of a class component isn't good because it creates new functions on every render.
e.g.
render() {
return (
<div onClick={() => console.log('this is bad')}>I'm dangerous</div>
)
}
But in my case, what I what to do is have a function (foo) that receives a parameter and set the state with that parameter. And I have multiple elements, each one with a different value for calling foo.
e.g.
foo = value => this.setState({value})
render() {
return (
<div>
<div onClick={() => this.foo(1)}>1</div> // this is also bad
<div onClick={() => this.foo(2)}>2</div> // this is also bad
<div onClick={() => this.foo(3)}>3</div> // this is also bad
<div onClick={() => this.foo(4)}>4</div> // this is also bad
<div onClick={() => this.foo(5)}>5</div> // this is also bad
</div>
)
}
So I need a walkaround on this, I need to call the same function only passing different arguments.
What I try to come up with is
Creating a function that returns a function
foo = value => () => this.setState({value})
render() {
return (
<div>
<div onClick={this.foo(1)}>1</div>
<div onClick={this.foo(2)}>2</div>
<div onClick={this.foo(3)}>3</div>
<div onClick={this.foo(4)}>4</div>
<div onClick={this.foo(5)}>5</div>
</div>
)
}
But I think this is just as bad as the first example
Creating on function for each value
foo1 = value => this.setState({value})
foo2 = value => this.setState({value})
foo3 = value => this.setState({value})
foo4 = value => this.setState({value})
foo5 = value => this.setState({value})
render() {
return (
<div>
<div onClick={this.foo1}>1</div>
<div onClick={this.foo2}>2</div>
<div onClick={this.foo2}>3</div>
<div onClick={this.foo4}>4</div>
<div onClick={this.foo5}>5</div>
</div>
)
}
This isn't bad in the case where you create a new function on every render, but this way, my component just becomes huge and very bad.
Giving this situation, How can I make this function or rendering without creating new functions on every render and don't create one function for each element value?
Just keep in mind that the real values in my case aren't numeric numbers (1234), the element of the onClick isn't a simple div element and foo doesn't only call this.setState, but do other stuff too. it's just an example.
The way you solve this in class-based components is to bind the function when the class itself is created.
class C {
foo = () => {
this.setState({...})
}
render() {
return <div onClick={this.foo} />
}
}
This will ensure that there is only one instance of that function per instance of the class. I would only recommend doing this if you have demonstrated there is a performance concern with having functions created in each render call.
I could add a data-attribute to the div, then use the event.target inside the onClick as Nick Parsons said, and based on his comment I came with another solution that maybe it's better.
I could create an component that receives a value and a onClick prop and inside the component I would just call this.prop.onClick(this.props.value). this way I would pass only one function to each component.
It's like data-attribute but with different props.

Why do i need to bind this arrow function in scope of map?

I'm trying to display a array from an array object using onClick event on a li , i am using arrow function but i found out i needed to bind the function inside the scope of map , why is that? Isn't that the purpose of arrow function to eliminate the need of binding?
class ListRecipes extends React.Component {
showIngredients = (item) => {
console.log(item.ingredients)
}
render() {
const { list } = this.props;
let Recipe = list.map((item, index) => {
let boundClick = this.showIngredients.bind(this, item);
return <li key={index} onClick={boundClick}> {item.recipeName} </li>
})
return (
<div>
<ul>
{Recipe}
</ul>
</div>
)
}
}
Also what is the difference between this code that returns a new function from the above?
class ListRecipes extends React.Component {
showIngredients = (item) => (event) => {
console.log(item.ingredients)
}
render() {
const { list } = this.props;
let Recipe = list.map((item, index) => {
return <li key={index} onClick={this.showIngredients(item)}> {item.recipeName} </li>
})
return (
<div>
<ul>
{Recipe}
</ul>
</div>
)
}
}
When you write onClick={this.showIngredients(item)} you are not assigning a reference of function to onClick but calling it
You could change it to
let Recipe = list.map((item, index) => {
return <li key={index} onClick={() => this.showIngredients(item)}> {item.recipeName} </li>
})
Also with let boundClick = this.showIngredients.bind(this, item); you are binding the showIngredients function to the context and passing an extra argument to it and since bind returns a new function so , boundClick is a function which is then later assigned to onClick
I think this code is generally preferred:
let boundClick = this.showIngredients.bind(this, item);
You are binding the item to be passed to the showIngredients function. So you end up creating items.length number of separate function references, each bound to this and the current item.
Before anything else we need to understand that the passed argument in callback listener on onClick is event.
That mean
function myMethod(event) {
// only event is passed as argument
}
<div onClick={myMehtod}/>
So question is how can you pass additional argument then? Simple. Call your desired method from the listener.
function calculate(item){
// no event trace here
}
<div onClick={function(event) { calculate(item) }}/>
// OR better, fat arrow
<div onClick={event => calculate(item)}/>
Remember the fat arrow is always bind to this scope rather then to prototype.
class Component {
scopedMethod = () => {
this.name = 'John';
}
prototypeMethod() {
this.name = 'John';
}
render() {
return (
<div>
{/* works */}
<button onClick={this.scopedMethod}/>
{/* OOPS, 'this' is event rather then component */}
<button onClick={this.prototypeMethod}/>
{/* works */}
<button onClick={() => this.prototypeMethod()}/>
{/* works */}
<button onClick={this.prototypeMethod.bind(this)}/>
</div>
)
}
}
Hopefully that's brought a light in. So the question would be when and why I would to use scope binded method over prototype method and vice versa? Well, prototype methods are cheap. Always try to use them. However some developer are avoiding lambda function (generate function inside render) because it can cause performance issue. When you need to re-render your component 10x in second, then the new lambda is created 10x.

Categories

Resources