Skip to content

Instantly share code, notes, and snippets.

@mike-at-redspace
Last active November 2, 2022 19:23
Show Gist options
  • Save mike-at-redspace/bf75716feea84f4d0d7b2c9c15e2c666 to your computer and use it in GitHub Desktop.
Save mike-at-redspace/bf75716feea84f4d0d7b2c9c15e2c666 to your computer and use it in GitHub Desktop.
useScrollSpy
import React, { useRef } from 'react'
const App = () => {
const sectionRefs = [useRef(null), useRef(null), useRef(null)]
const activeSection = useScrollSpy({
sectionElementRefs: sectionRefs,
offsetPx: -80
})
return (
<>
<nav className='App-navigation'>
<span className={`item ${activeSection === 0 ? ' active' : ''}`}>
Section 1
</span>
<span className={`item ${activeSection === 1 ? ' active' : ''}`}>
Section 2
</span>
<span className={`item ${activeSection === 2 ? ' active' : ''}`}>
Section 3
</span>
</nav>
<section className='App-section' ref={sectionRefs[0]}>
<h1>Section 1</h1>
</section>
<section className='App-section' ref={sectionRefs[1]}>
<h1>Section 2</h1>
</section>
<section className='App-section' ref={sectionRefs[2]}>
<h1>Section 3</h1>
</section>
</>
)
}
export default App
import { useState, useEffect } from "react";
const throttle = (callback, wait, immediate = false) => {
let timeout = null;
let initialCall = true;
return function () {
const callNow = immediate && initialCall;
const next = () => {
callback.apply(this, arguments);
timeout = null;
};
if (callNow) {
initialCall = false;
next();
}
if (!timeout) {
timeout = setTimeout(next, wait);
}
};
};
export default ({
activeSectionDefault = 0,
offsetPx = 0,
scrollingElement,
sectionElementRefs = [],
throttleMs = 100
}) => {
const [activeSection, setActiveSection] = useState(activeSectionDefault);
const handle = throttle(throttleMs, () => {
let currentSectionId = activeSection;
for (let i = 0; i < sectionElementRefs.length; i += 1) {
const section = sectionElementRefs[i].current;
// Needs to be a valid DOM Element
if (!section || !(section instanceof Element)) continue;
// GetBoundingClientRect returns values relative to viewport
if (section.getBoundingClientRect().top + offsetPx < 0) {
currentSectionId = i;
continue;
}
// No need to continue loop, if last element has been detected
break;
}
setActiveSection(currentSectionId);
});
useEffect(() => {
const scrollable = scrollingElement?.current ?? window;
scrollable.addEventListener("scroll", handle);
// Run initially
handle();
return () => {
scrollable.removeEventListener("scroll", handle);
};
}, [sectionElementRefs, offsetPx, scrollingElement, handle]);
return activeSection;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment