reactjsflickity

flickity slider ref undefined


So I have this react flickity slider component.

On first render the flickityRef returns undefined. If i change something in the file and the dev server updates the component. The flickityRef gets the correct ref.

My issue is that the onEngagement callback is not working before the component has done a re render.

Update

The above problem is now solved. Another issue is i render a component in the slider that has a filter. enter image description here

Normally you can click a slider element and it will slide to that element on click. But it does not work on initial render for this component as flickityRef is undefined. If i then click the filter and the slider re-renders. Or i save in dev mode and det hot reload re-renders. It works and flickityRef is something. I have not hade any luck finding the issue as it works when rendering other components in the slider.

Here is the new code:

import React, { useRef, useEffect, useCallback } from "react";
import { isBrowser } from "@cryos/helpers/browser.helper";
import Flickity from "flickity";

export interface ICarouselProps {
  content: JSX.Element[] | undefined;
  previewNext?: boolean;
  startIndex?: number;
  fullscreen?: boolean;
  cellAlign?: "left" | "right" | "center";
  onCarouselChangeCallback?: (index: number) => void;
  getFlickityRef?: (ref: Flickity) => void;
  /**
   * Triggered when certain events happen for tracking purposes.
   */
  onEngagement?: () => void;
  dontChangeSlideOnClick?: boolean;
}

export const Carousel = React.memo<ICarouselProps>((props) => {
  const {
    content,
    previewNext,
    onCarouselChangeCallback,
    startIndex,
    fullscreen,
    onEngagement,
    dontChangeSlideOnClick,
    getFlickityRef,
  } = props;

  const FlickityComponent = isBrowser ? require("react-flickity-component") : "div";

  const flickityRef = useRef<Flickity>();

  let cellAlignment = previewNext ? "left" : "center";
  cellAlignment = props.cellAlign ? props.cellAlign : cellAlignment;

  const handleOnEngagement = useCallback(() => {
    onEngagement?.();
  }, [onEngagement]);

  const setFlickityRef = useCallback(
    (ref: Flickity) => {
      flickityRef.current?.off("change", handleOnEngagement);

      flickityRef.current = ref;
      if (getFlickityRef) {
        getFlickityRef(ref);
      }

      flickityRef.current?.on("change", handleOnEngagement);
    },
    [handleOnEngagement, getFlickityRef],
  );

  useEffect(() => {
    const flickity = flickityRef.current;

    const onChange = (index?: number) => {
      if (typeof index !== "undefined" && onCarouselChangeCallback) {
        onCarouselChangeCallback(index);
      }
    };

    const onStaticClick = (_event?: Event, _pointer?: Element | Touch, _cellElement?: Element, cellIndex?: number) => {
      if (typeof cellIndex !== "undefined" && !dontChangeSlideOnClick && flickity) {
        flickity.select(cellIndex);
      }
    };

    if (flickity) {
      flickity.on("change", onChange);
      flickity.on("staticClick", onStaticClick);
    }

    return () => {
      if (flickity) {
        flickity.off("change", onChange);
        flickity.off("staticClick", onStaticClick);
      }
    };
  }, [content, onCarouselChangeCallback, dontChangeSlideOnClick]);

  return (
    <>
      {content && content.length > 1 ? (
        <div
          className={`c-carousel ${previewNext ? "c-carousel--preview" : ""} ${
            fullscreen ? "c-carousel--fullscreen" : ""
          }`}
        >
          <FlickityComponent
            flickityRef={setFlickityRef}
            className="c-carousel__flickity"
            options={{
              dragThreshold: 10,
              imagesLoaded: true,
              arrowShape: "M 20,50 L 70,100 L 75,95 L 30,50  L 75,5 L 70,0 Z",
              cellAlign: cellAlignment,
              initialIndex: startIndex || 0,
              setGallerySize: true,
            }}
          >
            {content}
          </FlickityComponent>
        </div>
      ) : (
        <>{content && content[0]}</>
      )}
    </>
  );
});

Carousel.displayName = "Carousel";

Solution

  • Please see if the following pattern addresses your use case. The idea is to subscribe to flickity events just one time inside the ref callback function.

    import * as React from "react";
    import Flickity from "react-flickity-component";
    
    import "flickity/css/flickity.css";
    
    type CarouselProps = {
      onChange?: () => void;
    };
    
    export default function Carousel({ onChange }: CarouselProps) {
      const flickityRef = React.useRef<Flickity>();
      const onChangeRef = React.useRef(onChange);
    
      React.useEffect(() => {
        onChangeRef.current = onChange;
      });
    
      const handleOnChange = React.useCallback(() => {
        onChangeRef.current?.();
      }, []);
    
      const setFlickityRef = React.useCallback((ref: Flickity) => {
        flickityRef.current?.off("change", handleOnChange);
        
        flickityRef.current = ref;
    
        flickityRef.current?.on("change", handleOnChange);
      }, []);
    
      return (
        <Flickity flickityRef={setFlickityRef}>
          <img src="https://picsum.photos/id/237/200/300" />
          <img src="https://picsum.photos/id/238/200/300" />
          <img src="https://picsum.photos/id/239/200/300" />
        </Flickity>
      );
    }
    

    https://codesandbox.io/p/sandbox/stackoverflow-76558602-xz83nz?file=%2Fsrc%2FCarousel.tsx