React .scrollLeft cannot be reading null value

Viewed 403

I am trying to access the horizontal scroll bar with buttons in react but turns out it is not working because of error that cannot read the null values of .scrollLeft.

<>
  <div className="containerOuterSider">
    <FaAngleLeft className= "FaAngleLeft" onClick={slide('left')}/>
    <div id="container3" className="container3">
      {products &&
        products
          .map((product) => (
            <AAsOfLowNav key={product._id} product={product} />
          ))
          .reverse()}
    </div>
      <FaAngleRight className="FaAngleRight" onClick={slide('right')}/>
  </div>
</>

and the main function is

function slide(direction) {
  var container = document.getElementById('container3');
  let scrollCompleted = 0;
  var slideVar = setInterval(function() {
    if (direction === 'left') {
      container.scrollLeft -= 10;
    } else {
      container.scrollLeft += 10;
    }
    scrollCompleted += 10;
    if (scrollCompleted >= 100) {
      window.clearInterval(slideVar);
    }
  }, 50);
}

The error I am facing is:

Uncaught TypeError: Cannot read properties of null (reading 'scrollLeft') at LowerCatNav.js:31:1 Uncaught TypeError: Cannot read properties of null (reading 'scrollLeft') at LowerCatNav.js:33:1

How can I make it right? Should I use the hooks or anything else?

2 Answers

Issue

You are immediately invoking the slide function while rendering:

<FaAngleLeft
  className="FaAngleLeft"
  onClick={slide('left')} // <-- immediately invoked when rendered
/>
...
<FaAngleRight
  className="FaAngleRight"
  onClick={slide('right')} // <-- immediately invoked when rendered
/>

The React component hasn't been fully rendered and pushed to the DOM, so queries to the DOM, i.e. document.getElementById('container3'), return null.

Solution

Fix the click handler so slide is not being immediately invoked.

<FaAngleLeft
  className="FaAngleLeft"
  onClick={() => slide('left')} // <-- asynchronously invoked when clicked
/>
...
<FaAngleRight
  className="FaAngleRight"
  onClick={() => slide('right')} // <-- asynchronously invoked when clicked
/>

It is considered a React anti-pattern to directly query the DOM for DOMNodes, use a React ref for this.

Example:

...

const containerRef = React.useRef(); // <-- (1) create Ref
const sliderTimerRef = React.useRef();

useEffect(() => {
  return () => {
    // clear any running intervals on component unmount
    clearInterval(sliderTimerRef.current);
  };
}, []);

...

function slide(direction) {
  // clear any previously set intervals and reset scrollCompleted
  clearInterval(sliderTimerRef.current);
  let scrollCompleted = 0;

  sliderTimerRef.current = setInterval(function() {
    const container = containerRef.current; // <-- (3) access current ref value

    if (direction === 'left') {
      container?.scrollLeft -= 10; // <-- (4)  Optional Chaining null check
    } else {
      container?.scrollLeft += 10; // <-- (4)  Optional Chaining null check
    }
    scrollCompleted += 10;
    if (scrollCompleted >= 100) {
      clearInterval(sliderTimerRef.current);
    }
  }, 50);
}

...

return (
  <>
    <div className="containerOuterSider">
      <FaAngleLeft className="FaAngleLeft" onClick={() => slide('left')}/>
      <div
        ref={containerRef} // <-- (2) attach ref to element
        id="container3"
        className="container3"
      >
        {products
          .map((product) => (
            <AAsOfLowNav key={product._id} product={product} />
          ))
          .reverse()
        }
      </div>
      <FaAngleRight className="FaAngleRight" onClick={() => slide('right')}/>
    </div>
  </>
);
  1. slide is called when #container3 is not yet created.
    Change onClick={slide(...)} to onClick={() => slide(...)}

  2. Access to global elements (e.g. by id) should be avoided.
    Prefer ref/createRef/useRef to access elements belonging to your component.

Related