rcbyr / keen-slider

The HTML touch slider carousel with the most native feeling you will get.
https://keen-slider.io/
MIT License
4.71k stars 214 forks source link

[NextJS 14.2.4] Keen slider not update when a page is hydrated by Next/Link Component #439

Open GabrielAtlas opened 2 months ago

GabrielAtlas commented 2 months ago

Basically, the bug happens when you navigate through the next/Link component in an application made with NextJS where Keen Slider fails to update the slide. Watch the video as an example

https://github.com/user-attachments/assets/9aab7f78-0300-4538-a249-5f9b5fb90ebf

My Code:

'use client'
import { useEffect, useState } from 'react'
import Image from 'next/image'

import { useCardapio } from '@/app/contexts/cardapio-context'
import { useKeenSlider } from 'keen-slider/react'

import fallbackIcon from '../../public/icons/popeyes-fallback-category-icon.svg'

export default function CardapioFilters() {
  const { categories, isLoading } = useCardapio()

  const [currentSlide, setCurrentSlide] = useState(0)
  const [loaded, setLoaded] = useState(false)

  const [sliderRef, instanceRef] = useKeenSlider<HTMLDivElement>({
    initial: 0,
    slideChanged(slider) {
      setCurrentSlide(slider.track.details.rel)
    },
    created() {
      setLoaded(true)
    },
    slides: {
      perView: 9,
      spacing: 10,
    },
    breakpoints: {
      '(max-width: 500px)': {
        slides: {
          perView: 3,
        },
      },
    },
  })

  const [isFixed, setIsFixed] = useState<boolean>(false)
  const scrollThreshold = 360 // Height of the header is the threshold

  useEffect(() => {
    const handleScroll = () => {
      if (window.scrollY > scrollThreshold) {
        setIsFixed(true)
      } else {
        setIsFixed(false)
      }
    }

    window.addEventListener('scroll', handleScroll)

    return () => {
      window.removeEventListener('scroll', handleScroll)
    }
  }, [])

  useEffect(() => {
    setTimeout(() => instanceRef.current?.update(), 10) // FIX: Keen Slider Bug (On Switch between NextJS Link Navigation)
  }, [instanceRef])

  const shouldArrowAppears =
    loaded && categories.length > 1 && instanceRef.current
  const slideCount = instanceRef.current?.track?.details?.slides?.length ?? 0

  if (isLoading)
    return <div className="min-h-24 bg-neutral-200 animate-pulse w-full" />

  return (
    <div
      className={`min-h-24 bg-white w-full ${isFixed ? 'fixed top-0 md:top-[96px]' : 'block'}`}
    >
      <div className="mx-auto max-w-[1288px] px-6 md:px-0 w-full">
        <div className="navigation-wrapper">
          <div ref={sliderRef} className="keen-slider w-full pt-2">
            {categories?.map((category, index) => {
              return (
                <div
                  key={`cardapio-category-${index}`}
                  className="keen-slider__slide flex flex-col items-center gap-2 text-center"
                >
                  <Image
                    src={category.imagem?.url || fallbackIcon}
                    width={48}
                    height={48}
                    className="opacity-30"
                    alt="Plk Icon"
                  />
                  <span className="font-bold opacity-50">
                    {category.nome.length >= 14
                      ? category.nome.split(' ')[0]
                      : category.nome}
                  </span>
                </div>
              )
            })}
          </div>
          {shouldArrowAppears && (
            <div className="hidden md:block">
              <Arrow
                left
                onClick={(e: React.MouseEvent<SVGSVGElement, MouseEvent>) => {
                  e.stopPropagation()
                  if (instanceRef.current) instanceRef.current.prev()
                }}
                disabled={currentSlide === 0}
              />

              <Arrow
                onClick={(e: React.MouseEvent<SVGSVGElement, MouseEvent>) => {
                  e.stopPropagation()
                  if (instanceRef.current) instanceRef.current.next()
                }}
                disabled={currentSlide === slideCount - 1}
              />
            </div>
          )}
        </div>
      </div>
    </div>
  )
}

function Arrow(props: {
  disabled: boolean
  left?: boolean
  onClick: (e: React.MouseEvent<SVGSVGElement, MouseEvent>) => void
}) {
  const disabled = props.disabled ? ' arrow--disabled' : ''
  const arrowDirection = props.left
    ? 'arrow-cardapio-left'
    : 'arrow-cardapio-right'

  return (
    <svg
      onClick={props.onClick}
      className={`arrow ${arrowDirection} ${disabled}`}
      xmlns="http://www.w3.org/2000/svg"
      width={40}
      height={40}
      viewBox="0 0 40 40"
    >
      {props.left && (
        <>
          <circle
            className="arrow-circle"
            cx="20"
            cy="20"
            r="20"
            fill="#E5E5E5"
          />
          <path
            className="arrow-path"
            d="M25.0019 10.9851C24.5119 10.4951 23.7219 10.4951 23.2319 10.9851L14.9219 19.2951C14.5319 19.6851 14.5319 20.3151 14.9219 20.7051L23.2319 29.0151C23.7219 29.5051 24.5119 29.5051 25.0019 29.0151C25.4919 28.5251 25.4919 27.7351 25.0019 27.2451L17.7619 19.9951L25.0119 12.7451C25.4919 12.2651 25.4919 11.4651 25.0019 10.9851Z"
            fill="#BDBDBD"
          />
        </>
      )}
      {!props.left && (
        <>
          <circle
            className="arrow-circle"
            cx="20"
            cy="20"
            r="20"
            fill="#E5E5E5"
          />
          <path
            className="arrow-path"
            d="M14.9982 29.0151C15.4882 29.5051 16.2782 29.5051 16.7682 29.0151L25.0782 20.7051C25.4682 20.3151 25.4682 19.6851 25.0782 19.2951L16.7682 10.9851C16.2782 10.4951 15.4882 10.4951 14.9982 10.9851C14.5082 11.4751 14.5082 12.2651 14.9982 12.7551L22.2382 20.0051L14.9882 27.2551C14.5082 27.7351 14.5082 28.5351 14.9982 29.0151Z"
            fill="#BDBDBD"
          />
        </>
      )}
    </svg>
  )
}
yanckst commented 1 month ago

I agree. I have the same problem using NextJs 14.2.13