14

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?

3 Answers 3

8

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.

Sign up to request clarification or add additional context in comments.

1 Comment

could you help me adapt that to a top bar menu? I would need to change only the css of the Aside?
3

Live running Demo

https://codesandbox.io/s/gallant-wing-ue2ks

Steps

1. Add the dependency

using npm

npm i react-scrollspy

or using yarn

yarn add react-scrollspy

2. Create your HTML sections

<section id="section-1">
    <!-- Content goes here -->
</section>
<section id="section-2">
    <!-- Content goes here -->
</section>

<!-- .... -->

<section id="section-n">
    <!-- Content goes here -->
</section>

3. Add those section's ID to Scrollspy items

<Scrollspy items={ ['section-1', 'section-2', ..., 'section-n'] } />

4. Add some styles

Check this documentation https://makotot.github.io/react-scrollspy/#section-3

Comments

2

Check out react-ui-scrollspy it is very easy to use.

Full disclosure: I maintain this package.

  1. In your navigation component
<div>
  <p data-to-scrollspy-id="first">Section 1</p>
  <p data-to-scrollspy-id="second">Section 2</p>
</div>
  1. Wrap the elements you want to spy on in the <ScrollSpy> component.
import ScrollSpy from "react-ui-scrollspy";

<ScrollSpy>
  <div id="first">
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Aut dolores
    veritatis doloremque fugit. Soluta aperiam atque inventore deleniti,
    voluptatibus non fuga eos magni natus vel, rerum excepturi expedita.
    Tempore, vero!
  </div>
  <div id="second">
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Aut dolores
    veritatis doloremque fugit. Soluta aperiam atque inventore deleniti,
    voluptatibus non fuga eos magni natus vel, rerum excepturi expedita.
    Tempore, vero!
  </div>
</ScrollSpy>
  1. Write styles for when the navigation element which is active in your index.css
.active-scroll-spy {
  background-color: yellowgreen;
  border-radius: 15px;
}

1 Comment

Throws an error if you try to use with react 18, just FYI.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.