From e5f3bbe0952514be4ed799b41c60cd7a89acf58f Mon Sep 17 00:00:00 2001 From: yohlo Date: Wed, 27 Aug 2025 09:58:55 -0500 Subject: [PATCH] optimize swipeable tabs --- src/components/swipeable-tabs.tsx | 42 +++++++++++++++---------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/components/swipeable-tabs.tsx b/src/components/swipeable-tabs.tsx index cab016a..bed674c 100644 --- a/src/components/swipeable-tabs.tsx +++ b/src/components/swipeable-tabs.tsx @@ -1,6 +1,6 @@ import { FloatingIndicator, UnstyledButton, Box, Text } from "@mantine/core"; import { Carousel } from "@mantine/carousel"; -import { useState, useEffect, ReactNode, useRef } from "react"; +import { useState, useEffect, ReactNode, useRef, useCallback, useMemo } from "react"; import { useRouter } from "@tanstack/react-router"; interface TabItem { @@ -19,22 +19,22 @@ function SwipeableTabs({ tabs, defaultTab = 0, onTabChange }: SwipeableTabsProps const search = router.state.location.search as any; const [embla, setEmbla] = useState(null); - const getActiveTabFromUrl = () => { + const getActiveTabFromUrl = useCallback(() => { const urlTab = search?.tab; if (typeof urlTab === 'string') { const tabIndex = tabs.findIndex(tab => tab.label.toLowerCase() === urlTab.toLowerCase()); return tabIndex !== -1 ? tabIndex : defaultTab; } return defaultTab; - }; + }, [search?.tab, tabs, defaultTab]); const [activeTab, setActiveTab] = useState(getActiveTabFromUrl); const [rootRef, setRootRef] = useState(null); - const [controlsRefs, setControlsRefs] = useState>({}); + const controlsRefs = useRef>({}); const slideRefs = useRef>({}); const [carouselHeight, setCarouselHeight] = useState('auto'); - const changeTab = (index: number) => { + const changeTab = useCallback((index: number) => { if (index === activeTab || index < 0 || index >= tabs.length) return; setActiveTab(index); @@ -47,19 +47,20 @@ function SwipeableTabs({ tabs, defaultTab = 0, onTabChange }: SwipeableTabsProps url.searchParams.set('tab', tabLabel); window.history.replaceState(null, '', url.toString()); } - }; + }, [activeTab, tabs, embla, onTabChange]); + + const handleEmblaSelect = useCallback(() => { + if (!embla) return; + const newIndex = embla.selectedScrollSnap(); + changeTab(newIndex); + }, [embla, changeTab]); useEffect(() => { if (!embla) return; - const onSelect = () => { - const newIndex = embla.selectedScrollSnap(); - changeTab(newIndex); - }; - - embla.on("select", onSelect); - return () => embla.off("select", onSelect); - }, [embla, activeTab, tabs]); + embla.on("select", handleEmblaSelect); + return () => embla.off("select", handleEmblaSelect); + }, [embla, handleEmblaSelect]); useEffect(() => { const newActiveTab = getActiveTabFromUrl(); @@ -77,14 +78,13 @@ function SwipeableTabs({ tabs, defaultTab = 0, onTabChange }: SwipeableTabsProps } }, [activeTab]); - const setControlRef = (index: number) => (node: HTMLSpanElement | null) => { - controlsRefs[index] = node; - setControlsRefs(controlsRefs); - }; + const setControlRef = useCallback((index: number) => (node: HTMLSpanElement | null) => { + controlsRefs.current[index] = node; + }, []); - const setSlideRef = (index: number) => (node: HTMLDivElement | null) => { + const setSlideRef = useCallback((index: number) => (node: HTMLDivElement | null) => { slideRefs.current[index] = node; - }; + }, []); return ( @@ -100,7 +100,7 @@ function SwipeableTabs({ tabs, defaultTab = 0, onTabChange }: SwipeableTabsProps }} >