Skip to content

Commit 31ec96f

Browse files
committed
perf: custom useMeasure hook, filter falsy children
1 parent a0d4781 commit 31ec96f

File tree

5 files changed

+116
-80
lines changed

5 files changed

+116
-80
lines changed

package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@kkx64/react-simple-carousel",
3-
"version": "1.4.0",
3+
"version": "1.4.1",
44
"description": "Simple carousel component for React",
55
"author": "Kiril Krsteski",
66
"homepage": "https://github.com/kkx64/react-simple-carousel",
@@ -41,8 +41,7 @@
4141
"build-storybook": "npm run build && storybook build"
4242
},
4343
"dependencies": {
44-
"classnames": "^2.5.1",
45-
"react-use-measure": "^2.1.1"
44+
"classnames": "^2.5.1"
4645
},
4746
"peerDependencies": {
4847
"react": "^18.2.0",

pnpm-lock.yaml

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

src/Carousel.tsx

Lines changed: 54 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
} from "react";
1515

1616
import clsx from "classnames";
17-
import useMeasure from "react-use-measure";
17+
import useMeasure from "./hooks/useMeasure";
1818

1919
import CarouselArrows from "./CarouselArrows";
2020
import CarouselDots, { DotRenderFnProps } from "./CarouselDots";
@@ -81,7 +81,8 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>(
8181
const containerReactRef = useRef<HTMLDivElement | null>(null);
8282
const autoPlayIntervalRef = useRef<NodeJS.Timeout | null>(null);
8383

84-
const [containerRef, containerBounds] = useMeasure({ debounce: 100 });
84+
const [containerRef, { width }] = useMeasure();
85+
const containerWidth = useMemo(() => width ?? 0, [width]);
8586

8687
const [currentSlide, setCurrentSlide] = useState(0);
8788

@@ -94,18 +95,25 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>(
9495
const scrollTimeout = useRef<NodeJS.Timeout | null>(null);
9596

9697
/** Maximum offset when dragging */
97-
const maxDragOffset = useMemo(
98-
() => containerBounds.width,
99-
[containerBounds.width],
100-
);
98+
const maxDragOffset = useMemo(() => containerWidth, [containerWidth]);
10199
/** Maximum offset when dragging out of bounds on last or first slide */
102100
const maxDragOffsetEnd = useMemo(
103-
() => containerBounds.width / 5,
104-
[containerBounds.width],
101+
() => containerWidth / 5,
102+
[containerWidth],
103+
);
104+
105+
const mappedChildren = useMemo(
106+
() => Children.toArray(children).filter(Boolean),
107+
[children],
105108
);
106109

107110
/** Total number of slides */
108-
const slides = useMemo(() => Children.count(children), [children]);
111+
const slides = useMemo(() => mappedChildren.length, [mappedChildren]);
112+
113+
const slideWidth = useMemo(
114+
() => containerWidth / shownSlides,
115+
[containerWidth, shownSlides],
116+
);
109117

110118
// Slide change logic
111119

@@ -142,11 +150,11 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>(
142150

143151
const translateX = useMemo(() => {
144152
const maxTranslateX =
145-
(slides - shownSlides) * (containerBounds.width / shownSlides);
153+
(slides - shownSlides) * (containerWidth / shownSlides);
146154
const calculatedTranslateX =
147-
currentSlide * (containerBounds.width / shownSlides);
155+
currentSlide * (containerWidth / shownSlides);
148156
return Math.min(calculatedTranslateX, maxTranslateX);
149-
}, [currentSlide, containerBounds.width, shownSlides, slides]);
157+
}, [currentSlide, containerWidth, shownSlides, slides]);
150158

151159
const handleDragStart = useCallback((event: MouseEvent | TouchEvent) => {
152160
const clientX =
@@ -196,7 +204,7 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>(
196204
dragStart.y,
197205
translateX,
198206
trackRef.current,
199-
containerBounds.width,
207+
containerWidth,
200208
scrolling,
201209
],
202210
);
@@ -211,13 +219,13 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>(
211219

212220
// Maximum allowable translation in pixels
213221
const maxTranslateX =
214-
(containerBounds.width * (slides - shownSlides)) / shownSlides;
222+
(containerWidth * (slides - shownSlides)) / shownSlides;
215223

216224
// Determine the new slide index based on drag offset
217225
let newSlide = currentSlide;
218-
if (dragOffset > containerBounds.width / 2) {
226+
if (dragOffset > containerWidth / 2) {
219227
newSlide = Math.max(0, currentSlide - 1);
220-
} else if (dragOffset < -containerBounds.width / 2) {
228+
} else if (dragOffset < -containerWidth / 2) {
221229
newSlide = Math.min(slides - shownSlides, currentSlide + 1);
222230
}
223231

@@ -229,18 +237,15 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>(
229237
if (trackRef.current) {
230238
const newTranslateX = Math.max(
231239
0,
232-
Math.min(
233-
maxTranslateX,
234-
(newSlide * containerBounds.width) / shownSlides,
235-
),
240+
Math.min(maxTranslateX, (newSlide * containerWidth) / shownSlides),
236241
);
237242
trackRef.current.style.transform = `translateX(-${newTranslateX}px)`;
238243
}
239244
},
240245
[
241246
dragging,
242247
dragStart.x,
243-
containerBounds.width,
248+
containerWidth,
244249
slides,
245250
shownSlides,
246251
currentSlide,
@@ -265,7 +270,7 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>(
265270
if (scrollTimeout.current) {
266271
clearTimeout(scrollTimeout.current);
267272
}
268-
scrollTimeout.current = setTimeout(handleScrollEnd, 500);
273+
scrollTimeout.current = setTimeout(handleScrollEnd, 300);
269274
};
270275

271276
const handleScrollEnd = () => {
@@ -325,8 +330,8 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>(
325330
return (
326331
<div
327332
ref={(node) => {
328-
containerRef(node);
329333
containerReactRef.current = node;
334+
containerRef(node);
330335
}}
331336
className={clsx("Carousel", containerClassName)}
332337
style={{
@@ -338,28 +343,32 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>(
338343
style={trackStyle}
339344
className={clsx("Carousel__track", trackClassName)}
340345
>
341-
{Children.map(children, (child, i) => (
342-
<div
343-
key={`slide-${i}`}
344-
data-index={i}
345-
style={{
346-
width: containerBounds.width / shownSlides,
347-
}}
348-
className={clsx({
349-
Carousel__slide: true,
350-
"Carousel__slide--dragging": dragging,
351-
"Carousel__slide--active": currentSlide === i,
352-
353-
...(slideClassName && {
354-
[slideClassName]: true,
355-
[`${slideClassName}--dragging`]: dragging,
356-
[`${slideClassName}--active`]: currentSlide === i,
357-
}),
358-
})}
359-
>
360-
{child}
361-
</div>
362-
))}
346+
{Children.toArray(children)
347+
.filter(Boolean)
348+
.map((child, i) => {
349+
return (
350+
<div
351+
key={`slide-${i}`}
352+
data-index={i}
353+
style={{
354+
width: slideWidth,
355+
}}
356+
className={clsx({
357+
Carousel__slide: true,
358+
"Carousel__slide--dragging": dragging,
359+
"Carousel__slide--active": currentSlide === i,
360+
361+
...(slideClassName && {
362+
[slideClassName]: true,
363+
[`${slideClassName}--dragging`]: dragging,
364+
[`${slideClassName}--active`]: currentSlide === i,
365+
}),
366+
})}
367+
>
368+
{child}
369+
</div>
370+
);
371+
})}
363372
</div>
364373
{customDots &&
365374
customDots({

src/CarouselDots.tsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import "./CarouselDots.scss";
33
import { memo, useCallback, useMemo } from "react";
44

55
import clsx from "classnames";
6-
import useMeasure from "react-use-measure";
6+
import useMeasure from "./hooks/useMeasure";
77

88
export interface DotRenderFnProps {
99
dot: number;
@@ -38,27 +38,32 @@ const CarouselDots = ({
3838
onDotClick: onDotClickProp,
3939
dotRender,
4040
}: CarouselDotsProps) => {
41-
const [containerRef, containerBounds] = useMeasure({ debounce: 100 });
42-
const [trackRef, trackBounds] = useMeasure({ debounce: 100 });
43-
const [dotRef, dotBounds] = useMeasure({ debounce: 100 });
41+
const [containerRef, containerBounds] = useMeasure();
42+
const containerWidth = containerBounds.width ?? 0;
43+
44+
const [trackRef, trackBounds] = useMeasure();
45+
const trackWidth = trackBounds.width ?? 0;
46+
47+
const [dotRef, dotBounds] = useMeasure();
48+
const dotWidth = dotBounds.width ?? 0;
4449

4550
const dotsArray = useMemo(
4651
() => Array.from({ length: dots }, (_, index) => index),
4752
[dots],
4853
);
4954

5055
const dotGap = useMemo(
51-
() => (trackBounds.width - dotBounds.width * dots) / (dots - 1),
52-
[dotBounds.width],
56+
() => (trackWidth - dotWidth * dots) / (dots - 1),
57+
[dotWidth],
5358
);
5459

5560
const translateOffsetLeft = useMemo(
56-
() => containerBounds.width / 2 - dotBounds.width / 2,
57-
[containerBounds.width, trackBounds.width],
61+
() => containerWidth / 2 - dotWidth / 2,
62+
[containerWidth, trackWidth],
5863
);
5964

6065
const translateX = useMemo(
61-
() => translateOffsetLeft - (dotBounds.width + dotGap) * activeDot,
66+
() => translateOffsetLeft - (dotWidth + dotGap) * activeDot,
6267
[translateOffsetLeft, activeDot, dots],
6368
);
6469

@@ -69,9 +74,7 @@ const CarouselDots = ({
6974
return (
7075
<div
7176
style={{
72-
width: fixed
73-
? "fit-content"
74-
: `min(${dotBounds.width * 3 + 36}px, 80%)`,
77+
width: fixed ? "fit-content" : `min(${dotWidth * 3 + 36}px, 80%)`,
7578
}}
7679
className={clsx("CarouselDots", wrapperClassName)}
7780
>

src/hooks/useMeasure.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { MutableRefObject, useEffect, useRef, useState } from "react";
2+
3+
const useMeasure = () => {
4+
const observer = useRef<ResizeObserver | null>(null);
5+
const measureRef: MutableRefObject<HTMLElement | null> =
6+
useRef<HTMLElement | null>(null);
7+
8+
const setRef = (element: HTMLElement | null) => {
9+
measureRef.current = element;
10+
};
11+
12+
const [width, setWidth] = useState<number | null>(null);
13+
const [height, setHeight] = useState<number | null>(null);
14+
15+
const onResize = ([entry]: ResizeObserverEntry[]) => {
16+
if (!entry) return;
17+
if (entry.contentRect.width !== width) setWidth(entry.contentRect.width);
18+
if (entry.contentRect.height !== height)
19+
setHeight(entry.contentRect.height);
20+
};
21+
22+
useEffect(() => {
23+
if (observer && observer.current && measureRef.current) {
24+
observer.current.unobserve(measureRef.current);
25+
}
26+
observer.current = new ResizeObserver(onResize);
27+
observe();
28+
29+
return () => {
30+
if (observer.current && measureRef.current) {
31+
observer.current.unobserve(measureRef.current);
32+
}
33+
};
34+
}, [measureRef.current]);
35+
36+
const observe = () => {
37+
if (measureRef.current && observer.current) {
38+
observer.current.observe(measureRef.current);
39+
}
40+
};
41+
42+
return [setRef, { width, height }] as const;
43+
};
44+
45+
export default useMeasure;

0 commit comments

Comments
 (0)