Why can't I manipulate the DOM directly with React refs in my project? [duplicate] - reactjs

This question already has answers here:
How can I use multiple refs for an array of elements with hooks?
(17 answers)
Closed 1 year ago.
Since my pathfinding project has too many cells for me to animate it by changing state I was told to use ref. I went with the forward ref approach, which I assume works becase in devtools each cell appears with the forwardRef tag.
Even when I try to console.log each ref it points me to the correct cell in the grid. The problem is when I try to mutate a property like className/bgColor, program crashes and gives me a TypeError: Cannot add property className, object is not extensible.
const node = (props, ref) => {
return (
<div ref={ref}>
</div >
)
}
const Node = React.forwardRef(node);
export default Node
function Grid() {
// How I created the refs
const refCollection = useRef(matrix.map(rows => rows.map(nodes => React.createRef())));
let grid = matrix.map((rows, i) => rows.map((_, j) => <Node
ref={refCollection.current[i][j]}
></Node>))
function createMazeAndAnimate() {
const orderCarved = generateMaze(rows, cols, setWall, setStartNode,
setGoalNode, setTraversed, setCarvedOrder);
orderCarved.forEach(([y, x], i) => {
setTimeout(() => {
refCollection.current[y][x].className = 'traversed'; //PROGRAM CRASHES HERE
console.log(refCollection.current[y][x]); // IF I JUST LOG THIS LINE I CAN
// SEE EACH CELL REFERENCED
// CORRECTLY
}, i * 20);
})
}
return (
<>
<Toolbar
generateMaze={createMazeAndAnimate}
/>
<div
className='grid'
>
{grid.map((row, i) => <div key={i} className='board-row'>{row}</div>)}
</div>
</>
)
}
Thank You!

You're trying to modify refs directly. You don't mutate a ref object itself, you mutate whatever's in ref.current. So you want refCollection.current[y][x].current.
Putting a ref inside a ref is a code smell, as a ref itself is designed to be mutated, putting it inside something else you mutate seems extraneous. You might want to put your ref array in state instead.

Related

how to get the height of the children from a parent? - React

Sorry for the question.
I am new to react.
I want to get the height of each child then apply the maximum one on all of them.
by the way i do it, i get always the height of the last child.
on other hand i don't know exactly how to force the maximum height on all the children.
i really appreciate the help.
here is my code :
for the parent component :
export default function Parent({data, index}) {
const [testHeight, setTestHeight] = useState([]);
const listRef = useRef(null);
useEffect (() => {
setTestHeight(listRef.current.clientHeight)
})
const {
objects: blocs
} = data
return (
<>
{blocs && Object.values(blocs).map((itemsBlocks, i) => (
<ItemChild dataItem={itemsBlocks}
ref={listRef}
maxHeight= { testHeight}
/>
))}
</>
)
}
for the child component :
const Child = forwardRef(function Child ({dataItem, maxHeight}, ref) {
useEffect(() => {
console.log(ref.current.clientHeight);
})
const {
description,
title
} = dataItem || {}
return (
<>
<div className="class_child" ref={ref} >
{maxHeight}
<p> {title} </p>
<p> {description} </p>
</div>
</>
)
});
export default Child
You have multiple issues.
First off, you are storing the height, from the parent, and, you seem to be giving the same ref to all of your children ?
Each component should have their own ref.
If you want to get all height, you need to change this :
useEffect (() => {
setTestHeight(listRef.current.clientHeight)
})
You are replacing the value of your testHeight unstead of adding values to the array.
You should instead, do this :
useEffect (() => {
setTestHeight((current ) => [...current, listRef.current.clientHeight])
})
I advise you to look how to update hooks by using function, as in my reply, to avoid a lot of later trouble you might meet :)
But, in my opinion, it would be better to update the hook from the children, I'm not sure if you need ref for anything, since you are new to react, I would believe you do not. If you don't need it, then, pass the setTestHeight to children, update it how I showed your, and then you will get all your height, and from the parent, by seeing when your array is full, you will be able to determine your maxheight (that you should store in another hook) and then, update all your children max height
I'm also not sure why your are using forwardref though.

React - Alternatives to triggering rerender of specific components after child mounting finishes

This is more of a conceptual question than anything else.
I'm trying to draw a SVG line between two elements in my application. The way I'm currently doing this is by having a top level ref, inside of which I store a ref for each of the child elements indexed by an arbitrary key. I then draw the arrows, deduced from a 2 dimensional array of pairs of these keys, look up the ref, and use the positioning of those elements to create the SVG's coordinates.
The problem with this method is, besides initial render, every render update afterwards uses outdated ref data as the children get rendered (and therefore positioned) after the parent, inside which the SVG layer is contained.
I've already thought of the obvious answers, useEffects, setStates, etc., but there doesn't seem to be a good solution here that I can think of. A lot of the obvious answers don't necessarily work in this case since they cause render loops. Another solution that I initially thought of was breaking down the arrow rendering to within each of the children, but this had problems of its own that initially caused the transition to having all of the refs live at the parent level.
My current solution is to key the SVG Layer component to a state variable, and then change the value of this variable in a useEffect once the final item renders, but this solution feels messy and improper.
Not exact but here's some code of the issue
Parent Component:
export default ({ items }) => {
const [svgKey, setSvgKey] = useState(0);
const pairs = items.reduce((arr, item) => {
for (const subItem of item.subItems) {
arr.push([subItem, item]);
}
}, [])
const itemRefs = useRef({});
return (
<>
{items.map(item => <Row items={items} setSvgKey={setSvgKey} item={item} refs={refs} key={item.key} />}
<SVGLayer key={svgKey} pairs={pairs} refs={refs} />
</>
);
}
SVG Layer
export default ({ pairs, refs }) => (
<svg>
{pairs.map(([a, b], i) => (
<Arrow key={i} a={refs.current[a.key]} b={refs.current[b.key]} />
)}
</svg>
);
Arrow
export default ({ a, b }) => <path d={[math for line coords here]} />;
Row
export default ({ refs, item, items, setSvgKey }) => {
useEffect(() => {
if (item.key === items[items.length - 1].key) {
setSvgKey(key => ++key);
}
});
return (
<div ref={el => (refs.current[item.key] = el)} />
);
}

Conditionally assign ref in react

I'm working on something in react and have encountered a challenge I'm not being able to solve myself. I've searched here and others places and I found topics with similar titles but didn't have anything to do with the problem I'm having, so here we go:
So I have an array which will be mapped into React, components, normally like so:
export default ParentComponent = () => {
//bunch of stuff here and there is an array called arr
return (<>
{arr.map((item, id) => {<ChildComponent props={item} key={id}>})}
</>)
}
but the thing is, there's a state in the parent element which stores the id of one of the ChildComponents that is currently selected (I'm doing this by setting up a context and setting this state inside the ChildComponent), and then the problem is that I have to reference a node inside of the ChildComponent which is currently selected. I can forward a ref no problem, but I also want to assign the ref only on the currently selected ChildComponent, I would like to do this:
export default ParentComponent = () => {
//bunch of stuff here and there is an array called arr and there's a state which holds the id of a selected ChildComponent called selectedObjectId
const selectedRef = createRef();
return (<>
<someContextProvider>
{arr.map((item, id) => {
<ChildComponent
props={item}
key={id}
ref={selectedObjectId == id ? selectedRef : null}
>
})}
<someContextProvider />
</>)
}
But I have tried and we can't do that. So how can dynamically assign the ref to only one particular element of an array if a certain condition is true?
You can use the props spread operator {...props} to pass a conditional ref by building the props object first. E.g.
export default ParentComponent = () => {
const selectedRef = useRef(null);
return (
<SomeContextProvider>
{arr.map((item, id) => {
const itemProps = selectedObjectId == id ? { ref: selectedRef } : {};
return (
<ChildComponent
props={item}
key={id}
{...itemProps}
/>
);
})}
<SomeContextProvider />
)
}
You cannot dynamically assign ref, but you can store all of them, and access by id
export default ParentComponent = () => {
//bunch of stuff here and there is an array called arr and theres a state wich holds the id of a selected ChildComponent called selectedObjectId
let refs = {}
// example of accessing current selected ref
const handleClick = () => {
if (refs[selectedObjectId])
refs[selectedObjectId].current.click() // call some method
}
return (<>
<someContextProvider>
{arr.map((item, id) => {
<ChildComponent
props={item}
key={id}
ref={refs[id]}
>
})}
<someContextProvider />
</>)
}
Solution
Like Drew commented in Medets answer, the only solution is to create an array of refs and access the desired one by simply matching the index of the ChildElement with the index of the ref array, as we can see here. There's no way we found to actually move a ref between objects, but performance cost for doing this should not be relevant.

Getting props of a clicked element in React

Currently learning React and need to get a prop of a clicked element. I managed to get it, but it does not feel like the "right" react way.
This is what I have (it works):
filterProjects = (event) => {
const value = event.currentTarget.getAttribute("filterTarget")
console.log(value)
}
Initially I tried multiple things, e.g.:
const value = this.props.filterTarget or const value = event.currentTarget.this.props.filterTarget or even using ref but all returned undefined when console logging the value.
(Using this.props as it's part of a class Component.)
This is what my target element looks like:
const categories = data.allPrismicProjectCategory.edges.map((cat, index) => {
return (
<a
key={index}
onClick={this.filterProjects}
filterTarget={cat.node.uid}
>
{cat.node.data.category.text}
</a>
)
})
One simple way is passing the value itself,
onClick={() => this.filterProjects(cat.node.uid)}
And the function,
filterProjects = (value) => {
console.log(value)
}

React: how to forward refs to multiple children?

I'm trying to forward multiple refs to multiple children DOM nodes:
I need references to the 7 buttons, so I can manage focus between them.
I tried by using an array of React.createRef() and then attach each element of this array to a child using index, but all refs refers to the last button.
Why and is there another solution?
class RestaurantsList extends Component {
references = Array(7).fill(React.createRef());
render() {
return (
<ul
id="restaurants-list"
role="menu"
>
{
this.props.restaurants.map((restaurant, index) => {
return (
<Restaurant
ref={this.references[index]}
name={restaurant.name}
/>
);
})
}
</ul>
);
}
}
const Restaurant = React.forwardRef((props, ref) => {
return (
<li>
<button
ref={ref}
>
{name}
</button>
</li>
);
})
As it was discussed in comments the best way is to keep list of references as an array or object property inside parent component.
As for Array.prototype.fill() its arguments is calculated just once. In other words fill(React.createRef()) will generate list where each entry will refer to the same object - and you will get equal ref to last element. So you need to use .map() for getting unique reference objects.
references = Array(7).fill(0).map(() => React.createRef());
Anyway in real world project this will rather happen in constructor() or componentDidUpdate().
But I believe it's better to have hashmap:
references = {};
getOrCreateRef(id) {
if (!this.references.hasOwnProperty(id)) {
this.references[id] = React.createRef();
}
return this.references[id];
}
render() {
return (
....
{
this.props.restaurants.map((restaurant, index) => {
return (
<Restaurant
ref={this.getOrCreateRef(restaurant.id)}
key={restaurant.id}
name={restaurant.name}
/>
);
})
}
Also you will need some helper methods to avoid exposing this.references to outer world:
focusById(id) {
this.references[id].current && this.references[id].current.focus();
}
And take special attention to cleaning up references to unmounted elements. Otherwise you may got memory leak if list of restaurants is changed dynamically(if ref stays in this.references it keeps reference to HTML element even if it has been detached). Actual need depends on how is your component used. Also this memory leakage will be fixed once container(that has reference = {}) is unmounted itself due to navigating away.

Resources