9

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.

  1. 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>
        );
    }
    
  2. 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?

timo
  • 2,119
  • 1
  • 24
  • 40

3 Answers3

8

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>
Jatin Parmar
  • 2,759
  • 5
  • 20
  • 31
  • Ah, I figured I'd have to put a ref on each individial element. Did not know I could query trough the child elements like that. That's perfect for the DOM targeting part.(Got an error on `this.parentContainer` btw, should be `this.ParentContainer` capitalized). Any thoughts on how I can work around having to use the `let counter` inside the render function? – timo Dec 10 '19 at 13:33
  • 1.should be this.ParentContainer capitalized : yes 2.Any thoughts on how I can work around having to use the let counter inside the render function? i have tried to remove it on given fiddle – Jatin Parmar Dec 10 '19 at 13:41
  • Maybe I'm looking at the wrong version of your fiddle, but I'm talking about the lines 49-61 . Preferably I'd move these outside of the render function. Perhaps this is out of scope of the original question. – timo Dec 10 '19 at 14:32
  • The solution for the counter part is inefficient for when there are a lot of `sections`. I'd like to keep the function generic. From the question: `(in the live code I've got about 200 of these elements so setting the array item number for them manually is not efficient).` Perhaps counting inside the render function is the most efficient way after all. – timo Dec 11 '19 at 16:21
1

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.

Masih Jahangiri
  • 9,489
  • 3
  • 45
  • 51
  • Could you provide a Fiddle with this? Also, could you provide info about my stated comment in the question about using references? : ""... but I believe references should be kept to a minimum and as stated, the actual code consists of hundreds of these sections..." – timo Dec 06 '19 at 08:06
1

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}
    />
  )
})
93sauu
  • 3,770
  • 3
  • 27
  • 43