Skip to content

Commit 8f96bcf

Browse files
committed
🐛 FIX: unnecessary updates to class list
1 parent c281e4b commit 8f96bcf

File tree

11 files changed

+200
-18
lines changed

11 files changed

+200
-18
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ yarn add react-ui-scrollspy
3737

3838
2. Wrap the elements you want to spy on in the `<ScrollSpy>` component.
3939

40+
<!-- prettier-ignore -->
4041
```tsx
4142
import ScrollSpy from "react-ui-scrollspy";
4243

@@ -53,7 +54,7 @@ import ScrollSpy from "react-ui-scrollspy";
5354
voluptatibus non fuga eos magni natus vel, rerum excepturi expedita.
5455
Tempore, vero!
5556
</div>
56-
</ScrollSpy>;
57+
</ScrollSpy>
5758
```
5859

5960
3. Write styles for when the navigation element which is active in your `index.css`

demo-app/src/App.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import Center from "./components/Center/Center";
22
import Navigation from "./components/Navigation/Navigation";
33
import ScrollSpy from "react-ui-scrollspy";
4+
import ScrollSpyDev from "./components/src";
45

56
function App() {
67
return (
78
<div>
89
<Navigation />
910

10-
<ScrollSpy scrollThrottle={100}>
11+
<ScrollSpyDev scrollThrottle={100}>
1112
<Center id="orange" backgroundColor={"orange"}>
1213
<h1>Orange</h1>
1314
</Center>
@@ -20,7 +21,7 @@ function App() {
2021
<Center id="green" backgroundColor={"green"}>
2122
<h1>Green</h1>
2223
</Center>
23-
</ScrollSpy>
24+
</ScrollSpyDev>
2425
</div>
2526
);
2627
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import * as React from "react";
2+
import {
3+
MutableRefObject,
4+
ReactNode,
5+
useEffect,
6+
useRef,
7+
useState,
8+
} from "react";
9+
import { isVisible } from "../utils/isVisible";
10+
import { throttle } from "../utils/throttle";
11+
12+
interface ScrollSpyProps {
13+
children: ReactNode;
14+
navContainerRef?: MutableRefObject<HTMLDivElement | null>;
15+
scrollThrottle?: number;
16+
}
17+
18+
const ScrollSpy = ({
19+
children,
20+
navContainerRef,
21+
scrollThrottle = 300,
22+
}: ScrollSpyProps) => {
23+
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
24+
const [navContainerItems, setNavContainerItems] = useState<NodeListOf<Element> | undefined>(); // prettier-ignore
25+
26+
// keeps track of the Id in navcontainer which is active
27+
// so as to not update classLists unless it has been updated
28+
const prevIdTracker = useRef("");
29+
30+
// To get the nav container items depending on whether the parent ref for the nav container is passed or not
31+
useEffect(() => {
32+
if (navContainerRef) {
33+
setNavContainerItems(
34+
navContainerRef.current?.querySelectorAll("[data-to-scrollspy-id]")
35+
);
36+
} else {
37+
setNavContainerItems(document.querySelectorAll("[data-to-scrollspy-id]"));
38+
}
39+
40+
// eslint-disable-next-line react-hooks/exhaustive-deps
41+
}, [navContainerRef]);
42+
43+
// fire once after nav container items are set
44+
useEffect(() => {
45+
checkAndUpdateActiveScrollSpy();
46+
47+
// eslint-disable-next-line react-hooks/exhaustive-deps
48+
}, [navContainerItems]);
49+
50+
const checkAndUpdateActiveScrollSpy = () => {
51+
const scrollParentContainer = scrollContainerRef.current;
52+
53+
// if there are no children, return
54+
if (!(scrollParentContainer && navContainerItems)) return;
55+
56+
// loop over all children in scroll container
57+
for (let i = 0; i < scrollParentContainer.children.length; i++) {
58+
// get child element
59+
const child = scrollParentContainer.children.item(i);
60+
if (!child) continue;
61+
const useChild = child as HTMLDivElement;
62+
63+
// check if the element is in the viewport
64+
if (isVisible(useChild)) {
65+
// if so, get its ID
66+
const changeHighlightedItemId = useChild.id;
67+
68+
// if the element was same as the one currently active ignore it
69+
if (prevIdTracker.current === changeHighlightedItemId) return;
70+
71+
// now loop over each element in the nav Container
72+
navContainerItems.forEach((el) => {
73+
const attrId = el.getAttribute("data-to-scrollspy-id");
74+
75+
// if the element contains 'active' the class remove it
76+
if (el.classList.contains("active-scroll-spy")) {
77+
el.classList.remove("active-scroll-spy");
78+
}
79+
80+
// check if its ID matches the ID we got from the viewport
81+
// also make sure it does not already contain the 'active' class
82+
if (
83+
attrId === changeHighlightedItemId &&
84+
!el.classList.contains("active-scroll-spy")
85+
) {
86+
el.classList.add("active-scroll-spy");
87+
88+
console.log("update to", changeHighlightedItemId);
89+
prevIdTracker.current = changeHighlightedItemId;
90+
window.history.pushState({}, "", `#${changeHighlightedItemId}`);
91+
}
92+
});
93+
break;
94+
}
95+
}
96+
};
97+
98+
window.addEventListener(
99+
"scroll",
100+
throttle(checkAndUpdateActiveScrollSpy, scrollThrottle)
101+
);
102+
103+
return <div ref={scrollContainerRef}>{children}</div>;
104+
};
105+
106+
export default ScrollSpy;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import ScrollSpyDev from "./ScrollSpy/ScrollSpy";
2+
3+
export default ScrollSpyDev;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// to check if the element is in viewport
2+
export const isVisible = (el: HTMLElement) => {
3+
const rectInView = el.getBoundingClientRect();
4+
5+
// this decides how much of the element should be visible
6+
const leniency = window.innerHeight * 0.5;
7+
8+
return (
9+
rectInView.top + leniency >= 0 &&
10+
rectInView.bottom - leniency <= window.innerHeight
11+
);
12+
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export const throttle = (callback: () => void, limit: number) => {
2+
var tick = false;
3+
4+
return () => {
5+
if (!tick) {
6+
callback();
7+
tick = true;
8+
setTimeout(function () {
9+
tick = false;
10+
}, limit);
11+
}
12+
};
13+
};

demo-app/src/index.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ code {
1515
.active-scroll-spy {
1616
background-color: yellowgreen;
1717
border-radius: 15px;
18+
transition: all 0.5s;
1819
}
1920
.ss-item {
2021
padding: 30px;

dev.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
## To test package
2+
3+
make required updates in the `src` (root) directory.
4+
5+
```
6+
npm run build
7+
npm link
8+
cd demo-app
9+
npm link react-ui-scrollspy
10+
```

dist/index.js

Lines changed: 24 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)