I've created a script that activates on mouseover on a parent container and should move it's child elements away from the mouse. I've currently got it working but there are some parts of the code that seem to contradict what REACT code should look like. Especially two parts.
I'm using a counter in the render function so that each span gets it's correct custom property from state.customProperties
which is an array that updates the custom properties on mouseover of the parent element.
render() {
let counter = 0;
const returnCustomProp = function(that) {
counter++;
let x = 0;
if (that.state.customProperties[counter - 1]) {
x = that.state.customProperties[counter - 1].x;
}
let y = 0;
if (that.state.customProperties[counter - 1]) {
y = that.state.customProperties[counter - 1].y;
}
return "customProperty(" + x + " " + y + ")";
}
return (
<div onMouseMove={this._testFunction} id="ParentContainer">
<section custom={returnCustomProp(this)}>
<b>Custom content for part 1</b>
<i>Could be way different from all the other elements</i>
</section>
<section custom={returnCustomProp(this)}>
2
</section>
<section custom={returnCustomProp(this)}>
<div>
All content can differ internally so I'm unable to create a generic element and loop trough that
</div>
</section>
<section custom={returnCustomProp(this)}>
4
</section>
<section custom={returnCustomProp(this)}>
5
</section>
<section custom={returnCustomProp(this)}>
<div>
<b>
This is just test data, the actualy data has no divs nested inside secions
</b>
<h1>
6
</h1>
</div>
</section>
<section custom={returnCustomProp(this)}>
7
</section>
<section custom={returnCustomProp(this)}>
8
</section>
</div>
);
}
In the mousemove function I'm using document.getElementById
and querySelectorAll
to get all the section elements and compare the mouse coordinates from the mouse to the section elements coordinates.
var mouseX = e.pageX;
var mouseY = e.pageY;
var spans = document.getElementById('ParentContainer').querySelectorAll('section');
var theRangeSquared = 10 * 10;
var maxOffset = 5;
var newCustomProperties = [];
for (var i = 0; i < spans.length; i++) {
var position = spans[i].getBoundingClientRect();
var widthMod = position.width / 2;
var heightMod = position.height / 2;
var coordX = position.x + widthMod;
var coordY = position.y + heightMod + window.scrollY;
// Distance from mouse
var dx = coordX - mouseX;
var dy = coordY - mouseY;
var distanceSquared = (dx * dx + dy * dy);
var tx = 0,
ty = 0;
if (distanceSquared < theRangeSquared && distanceSquared !== 0) {
// Calculate shift scale (inverse of distance)
var shift = maxOffset * (theRangeSquared - distanceSquared) / theRangeSquared;
var distance = Math.sqrt(distanceSquared);
tx = shift * dx / distance;
ty = shift * dy / distance;
}
newCustomProperties.push({
x: tx,
y: ty
});
}
I have a feeling I'm going about this all wrong. I'm not sure how I could avoid the counter while keeping a generic returnCustomProp
function to return the properties for said element (in the live code I've got about 200 of these elements so setting the array item number for them manually is not efficient).
The second part feels hacky to target an element by ID that is inside the actual component. I feel like I should be able to target this without traversing trough the DOM. Referencing the section elements could be a solution, but I believe references should be kept to a minimum and as stated, the actual code consists of hundreds of these sections.
JSFIDDLE
The code does not do much more atm than update the custom="customProperty(0 0)"
property. You can see this happen trough the element inspector on mouseover.
Can I make this functionality work withouth having to count the <section>
elements inside the render function and without having to use document.querySelectorAll
?
AppendChild. The simplest, most well-known method of appending an element to the DOM is certainly the appendChild() .
React will update the entire Virtual DOM. It'll then compare the Virtual DOM before updating, with the one after updating, to identify what objects have been changed. It uses the Diffing Algorithm. Only the changed objects will get updated on the Real DOM.
When the ref attribute is used on an HTML element, the ref created in the constructor with React. createRef() receives the underlying DOM element as its current property. When the ref attribute is used on a custom class component, the ref object receives the mounted instance of the component as its current .
How do I target DOM elements inside a react component?
As per React's official docs, you can access the dom elements by using refs.
You have to create a ref by using the React.createRef()
call.
Should I avoid targeting DOM elements altogether?
Traversing the dom to fetch a particular dom element is not a good practice with React as it will cause a performance issue. However, react allows an alternative way to do the same by using createRef()
.
Can I make this functionality work without having to count the elements inside the render function and without having to use document.querySelectorAll?
Yes, consider the following steps to implement this:
Inside the constructor, create a ref for parentContainer like this:
this.ParentContainer=React.createRef();
Then use parentContainer ref in render:
<div onMouseMove={this._testFunction} id="ParentContainer"
ref={this.ParentContainer} >
Inside test component, use this.parentContainer as:
//replace this
//var spans = document.getElementById('ParentContainer').querySelectorAll('section');
//with this
var spans = this.parentContainer.current.childNodes;
You can check it here
EDIT
Any thoughts on how I can work around having to use the let counter inside the render function
You can define returnCustomProp
outside render like this:
(Here you will need to pass the index of each section instead of a this
reference)
returnCustomProp = (index)=> {
let x = 0;
if(this.state.customProperties[index]) {
x = this.state.customProperties[index].x;
}
let y = 0;
if(this.state.customProperties[index]) {
y = this.state.customProperties[index].y;
}
return "customProperty("+x+" "+y+")";
}
And use it with section like this:
<section custom={returnCustomProp(0)}>
<b>Custom content for part 1</b>
<i>Could be way different from all the other elements</i>
</section>
<section custom={returnCustomProp(1)}>
2
</section>
<section custom={returnCustomProp(2)}>
<div>
All content can differ internally so I'm unable to create a generic element and loop trough that
</div>
</section>
<section custom={returnCustomProp(3)}>
4
</section>
<section custom={returnCustomProp(4)}>
5
</section>
<section custom={returnCustomProp(5)}>
<div>
<b>
This is just test data, the actual data has no divs nested inside sections
</b>
<h1>
6
</h1>
</div>
</section>
<section custom={returnCustomProp(6)}>
7
</section>
<section custom={returnCustomProp(7)}>
8
</section>
You should not manipulate DOM directly because react process on virtual dom. According to React doc you should use Ref forwarding. For more detail please read this.
You can use the method findDOMNode()
of ReactDOM if you does not find other solution but how the documentation said:
findDOMNode
is an escape hatch used to access the underlying DOM node. In most cases, use of this escape hatch is discouraged because it pierces the component abstraction.
As example I want to share a use case of my app where I need the reference to the container DOM div to implement the drag&drop for the libraries that I was using.
Example:
import React, { forwardRef } from 'react'
import _ from 'lodash'
import { List } from 'office-ui-fabric-react'
import { Draggable } from 'react-beautiful-dnd'
import ReactDOM from 'react-dom'
export const BasicItemList = forwardRef((
{
items,
onClickItem,
innerRef,
...rest
}, ref) => {
const itemToCell = (i, idx) => {
return (
<Draggable draggableId={id} index={idx} key={idx}>
{
(provided, snapshot) => {
return (
<MyCell item={i}
onClick={onClickItem}
innerRef={provided.innerRef}
isDragging={snapshot.isDragging}
{...provided.dragHandleProps}
{...provided.draggableProps}
/>
)
}
}
</Draggable>
)
}
const refGetter = (comp) => {
const div = ReactDOM.findDOMNode(comp)
if (_.isFunction(ref)) {
ref(comp)
} else if (ref) {
ref.current = comp
}
if (_.isFunction(innerRef)) {
innerRef(div)
}
}
return (
<List items={items}
onRenderCell={itemToCell}
componentRef={refGetter}
{...rest}
/>
)
})
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With