Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement a scrollspy with React

I want to implement a scrollspy without the bootstrap.

I have checked a lot of code online, all of them are implemented by jQuery.

How to implement the scrollspy only with the power of React?

like image 210
leuction Avatar asked Feb 24 '18 19:02

leuction


1 Answers

I've made a React Wrapper (used with Render-Props):

⚡ Live Codepen Example


const {useState, useEffect, useCallback, useRef} = React;

/**
 *
 * @param {Object} scrollParent [DOM node of scrollable element]
 * @param {Array} _targetElements [Array of nodes to spy on]
 */
const spyScroll = (scrollParent, _targetElements) => {
  if (!scrollParent) return false;

  // create an Object with all children that has data-name attribute
  const targetElements =
    _targetElements ||
    [...scrollParent.children].reduce(
      (map, item) =>
        item.dataset.name ? { [item.dataset.name]: item, ...map } : map,
      {}
    );

  let bestMatch = {};

  for (const sectionName in targetElements) {
    if (Object.prototype.hasOwnProperty.call(targetElements, sectionName)) {
      const domElm = targetElements[sectionName];
      const delta = Math.abs(scrollParent.scrollTop - domElm.offsetTop); // check distance from top, takig scroll into account

      if (!bestMatch.sectionName) 
        bestMatch = { sectionName, delta };

      // check which delet is closest to "0"
      if (delta < bestMatch.delta) {
        bestMatch = { sectionName, delta };
      }
    }
  }

  // update state with best-fit section
  return bestMatch.sectionName;
};




/**
 * Given a parent element ref, this render-props function returns
 * which of the parent's sections is currently scrolled into view
 * @param {Object} sectionsWrapperRef [Scrollable parent node React ref Object]
 */
const CurrentScrolledSection = ({ sectionsWrapperRef, children }) => {
  const [currentSection, setCurrentSection] = useState();

  // adding the scroll event listener inside this component, and NOT the parent component, to prever re-rendering of the parent component when
  // the scroll listener is fired and the state is updated, which causes noticable lag.
  useEffect(() => {
    const wrapperElm = sectionsWrapperRef.current;
    if (wrapperElm) {
      wrapperElm.addEventListener('scroll', e => setCurrentSection(spyScroll(e.target)));
      setCurrentSection(spyScroll(wrapperElm));
    }

    // unbind
    return () => wrapperElm.removeEventListener('scroll', throttledOnScroll)
  }, []);

  return children({ currentSection });
};

function App(){
  const sectionsWrapperRef = useRef()
  return <CurrentScrolledSection sectionsWrapperRef={sectionsWrapperRef}>
    {({ currentSection }) => <div ref={sectionsWrapperRef}>
    <section 
      data-name="section-a" 
      className={currentSection === "section-a" ? 'active' : ''}
    >Section A</section>
    <section 
      data-name="section-b" 
      className={currentSection === "section-b" ? 'active' : ''}
    >Section B</section>
    <section 
      data-name="section-c" 
      className={currentSection === "section-c" ? 'active' : ''}
    >Section C</section>
    </div>
  }
  </CurrentScrolledSection>
}


ReactDOM.render(
  <App />,
  document.getElementById('root')
)
html, body, #root{ height: 95%; overflow:hidden; }

#root > div{ 
  padding-bottom:20em; 
  height: 100%; 
  overflow:auto; 
  box-sizing: border-box;
}

section{ 
  height: 50vh;
  border-bottom: 1px solid red;
}

section.active{ background:lightyellow; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.0/umd/react-dom.production.min.js"></script>
<div id='root'></div>

It's not perfect because scroll direction does matter, since it implies on the intentions of the user.

like image 108
vsync Avatar answered Sep 29 '22 07:09

vsync