Skip to content

Commit

Permalink
perf: custom useMeasure hook, filter falsy children
Browse files Browse the repository at this point in the history
  • Loading branch information
kkx64 committed May 21, 2024
1 parent a0d4781 commit 31ec96f
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 80 deletions.
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@kkx64/react-simple-carousel",
"version": "1.4.0",
"version": "1.4.1",
"description": "Simple carousel component for React",
"author": "Kiril Krsteski",
"homepage": "https://github.com/kkx64/react-simple-carousel",
Expand Down Expand Up @@ -41,8 +41,7 @@
"build-storybook": "npm run build && storybook build"
},
"dependencies": {
"classnames": "^2.5.1",
"react-use-measure": "^2.1.1"
"classnames": "^2.5.1"
},
"peerDependencies": {
"react": "^18.2.0",
Expand Down
20 changes: 0 additions & 20 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

99 changes: 54 additions & 45 deletions src/Carousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
} from "react";

import clsx from "classnames";
import useMeasure from "react-use-measure";
import useMeasure from "./hooks/useMeasure";

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

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

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

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

/** Maximum offset when dragging */
const maxDragOffset = useMemo(
() => containerBounds.width,
[containerBounds.width],
);
const maxDragOffset = useMemo(() => containerWidth, [containerWidth]);
/** Maximum offset when dragging out of bounds on last or first slide */
const maxDragOffsetEnd = useMemo(
() => containerBounds.width / 5,
[containerBounds.width],
() => containerWidth / 5,
[containerWidth],
);

const mappedChildren = useMemo(
() => Children.toArray(children).filter(Boolean),
[children],
);

/** Total number of slides */
const slides = useMemo(() => Children.count(children), [children]);
const slides = useMemo(() => mappedChildren.length, [mappedChildren]);

const slideWidth = useMemo(
() => containerWidth / shownSlides,
[containerWidth, shownSlides],
);

// Slide change logic

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

const translateX = useMemo(() => {
const maxTranslateX =
(slides - shownSlides) * (containerBounds.width / shownSlides);
(slides - shownSlides) * (containerWidth / shownSlides);
const calculatedTranslateX =
currentSlide * (containerBounds.width / shownSlides);
currentSlide * (containerWidth / shownSlides);
return Math.min(calculatedTranslateX, maxTranslateX);
}, [currentSlide, containerBounds.width, shownSlides, slides]);
}, [currentSlide, containerWidth, shownSlides, slides]);

const handleDragStart = useCallback((event: MouseEvent | TouchEvent) => {
const clientX =
Expand Down Expand Up @@ -196,7 +204,7 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>(
dragStart.y,
translateX,
trackRef.current,
containerBounds.width,
containerWidth,
scrolling,
],
);
Expand All @@ -211,13 +219,13 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>(

// Maximum allowable translation in pixels
const maxTranslateX =
(containerBounds.width * (slides - shownSlides)) / shownSlides;
(containerWidth * (slides - shownSlides)) / shownSlides;

// Determine the new slide index based on drag offset
let newSlide = currentSlide;
if (dragOffset > containerBounds.width / 2) {
if (dragOffset > containerWidth / 2) {
newSlide = Math.max(0, currentSlide - 1);
} else if (dragOffset < -containerBounds.width / 2) {
} else if (dragOffset < -containerWidth / 2) {
newSlide = Math.min(slides - shownSlides, currentSlide + 1);
}

Expand All @@ -229,18 +237,15 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>(
if (trackRef.current) {
const newTranslateX = Math.max(
0,
Math.min(
maxTranslateX,
(newSlide * containerBounds.width) / shownSlides,
),
Math.min(maxTranslateX, (newSlide * containerWidth) / shownSlides),
);
trackRef.current.style.transform = `translateX(-${newTranslateX}px)`;
}
},
[
dragging,
dragStart.x,
containerBounds.width,
containerWidth,
slides,
shownSlides,
currentSlide,
Expand All @@ -265,7 +270,7 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>(
if (scrollTimeout.current) {
clearTimeout(scrollTimeout.current);
}
scrollTimeout.current = setTimeout(handleScrollEnd, 500);
scrollTimeout.current = setTimeout(handleScrollEnd, 300);
};

const handleScrollEnd = () => {
Expand Down Expand Up @@ -325,8 +330,8 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>(
return (
<div
ref={(node) => {
containerRef(node);
containerReactRef.current = node;
containerRef(node);
}}
className={clsx("Carousel", containerClassName)}
style={{
Expand All @@ -338,28 +343,32 @@ const Carousel = forwardRef<CarouselRef, CarouselProps>(
style={trackStyle}
className={clsx("Carousel__track", trackClassName)}
>
{Children.map(children, (child, i) => (
<div
key={`slide-${i}`}
data-index={i}
style={{
width: containerBounds.width / shownSlides,
}}
className={clsx({
Carousel__slide: true,
"Carousel__slide--dragging": dragging,
"Carousel__slide--active": currentSlide === i,

...(slideClassName && {
[slideClassName]: true,
[`${slideClassName}--dragging`]: dragging,
[`${slideClassName}--active`]: currentSlide === i,
}),
})}
>
{child}
</div>
))}
{Children.toArray(children)
.filter(Boolean)
.map((child, i) => {
return (
<div
key={`slide-${i}`}
data-index={i}
style={{
width: slideWidth,
}}
className={clsx({
Carousel__slide: true,
"Carousel__slide--dragging": dragging,
"Carousel__slide--active": currentSlide === i,

...(slideClassName && {
[slideClassName]: true,
[`${slideClassName}--dragging`]: dragging,
[`${slideClassName}--active`]: currentSlide === i,
}),
})}
>
{child}
</div>
);
})}
</div>
{customDots &&
customDots({
Expand Down
27 changes: 15 additions & 12 deletions src/CarouselDots.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import "./CarouselDots.scss";
import { memo, useCallback, useMemo } from "react";

import clsx from "classnames";
import useMeasure from "react-use-measure";
import useMeasure from "./hooks/useMeasure";

export interface DotRenderFnProps {
dot: number;
Expand Down Expand Up @@ -38,27 +38,32 @@ const CarouselDots = ({
onDotClick: onDotClickProp,
dotRender,
}: CarouselDotsProps) => {
const [containerRef, containerBounds] = useMeasure({ debounce: 100 });
const [trackRef, trackBounds] = useMeasure({ debounce: 100 });
const [dotRef, dotBounds] = useMeasure({ debounce: 100 });
const [containerRef, containerBounds] = useMeasure();
const containerWidth = containerBounds.width ?? 0;

const [trackRef, trackBounds] = useMeasure();
const trackWidth = trackBounds.width ?? 0;

const [dotRef, dotBounds] = useMeasure();
const dotWidth = dotBounds.width ?? 0;

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

const dotGap = useMemo(
() => (trackBounds.width - dotBounds.width * dots) / (dots - 1),
[dotBounds.width],
() => (trackWidth - dotWidth * dots) / (dots - 1),
[dotWidth],
);

const translateOffsetLeft = useMemo(
() => containerBounds.width / 2 - dotBounds.width / 2,
[containerBounds.width, trackBounds.width],
() => containerWidth / 2 - dotWidth / 2,
[containerWidth, trackWidth],
);

const translateX = useMemo(
() => translateOffsetLeft - (dotBounds.width + dotGap) * activeDot,
() => translateOffsetLeft - (dotWidth + dotGap) * activeDot,
[translateOffsetLeft, activeDot, dots],
);

Expand All @@ -69,9 +74,7 @@ const CarouselDots = ({
return (
<div
style={{
width: fixed
? "fit-content"
: `min(${dotBounds.width * 3 + 36}px, 80%)`,
width: fixed ? "fit-content" : `min(${dotWidth * 3 + 36}px, 80%)`,
}}
className={clsx("CarouselDots", wrapperClassName)}
>
Expand Down
45 changes: 45 additions & 0 deletions src/hooks/useMeasure.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { MutableRefObject, useEffect, useRef, useState } from "react";

const useMeasure = () => {
const observer = useRef<ResizeObserver | null>(null);
const measureRef: MutableRefObject<HTMLElement | null> =
useRef<HTMLElement | null>(null);

const setRef = (element: HTMLElement | null) => {
measureRef.current = element;
};

const [width, setWidth] = useState<number | null>(null);
const [height, setHeight] = useState<number | null>(null);

const onResize = ([entry]: ResizeObserverEntry[]) => {
if (!entry) return;
if (entry.contentRect.width !== width) setWidth(entry.contentRect.width);
if (entry.contentRect.height !== height)
setHeight(entry.contentRect.height);
};

useEffect(() => {
if (observer && observer.current && measureRef.current) {
observer.current.unobserve(measureRef.current);
}
observer.current = new ResizeObserver(onResize);
observe();

return () => {
if (observer.current && measureRef.current) {
observer.current.unobserve(measureRef.current);
}
};
}, [measureRef.current]);

const observe = () => {
if (measureRef.current && observer.current) {
observer.current.observe(measureRef.current);
}
};

return [setRef, { width, height }] as const;
};

export default useMeasure;

0 comments on commit 31ec96f

Please sign in to comment.