LottieFiles / dotlottie-web

Official LottieFiles player for rendering Lottie and dotLottie animations in the web. Supports React, Vue, Svelte, SolidJS and Web Components.
https://developers.lottiefiles.com/docs/dotlottie-player/
MIT License
179 stars 11 forks source link

InvalidStateError: Failed to execute 'transferControlToOffscreen' on 'HTMLCanvasElement': Cannot transfer control from a canvas more than once. #326

Open theashraf opened 2 months ago

theashraf commented 2 months ago

DotLottieWorkerReact on Next.js is throwing the following error:

InvalidStateError: Failed to execute 'transferControlToOffscreen' on 'HTMLCanvasElement': Cannot transfer control from a canvas more than once.
image
flow3d commented 3 weeks ago

Hey @theashraf ! did you find anything useful?

alumag commented 3 weeks ago

We have the same issue. We use @lottiefiles/dotlottie-web@^0.34.0 with next@^14.2.13. (We preferred to use @lottiefiles/dotlottie-web over @lottiefiles/dotlottie-react due to an issue the react package has with config.layout)

Way to reproduce:

"use client";

import { DotLottieWorker, type Config } from "@lottiefiles/dotlottie-web";
import {
  type ComponentPropsWithoutRef,
  useEffect,
  useLayoutEffect,
  useRef,
} from "react";

type DotLottieAnimationProps = ComponentPropsWithoutRef<"canvas"> & {
  readonly src: NonNullable<Config["src"]>;
  readonly height: string;
  readonly width: string;
};

const useIsomorphicLayoutEffect =
  typeof window === "undefined" ? useEffect : useLayoutEffect;

function createLottieWorker(canvas: HTMLCanvasElement, src: string): DotLottieWorker {
  return new DotLottieWorker({
    workerId: "dotlottie-worker",
    canvas,
    backgroundColor: "transparent",
    layout: { fit: "fit-width", align: [0.5, 0.5] },
    renderConfig: {
      autoResize: true,
      freezeOnOffscreen: false,
      devicePixelRatio: 5,
    },
    src,
    loop: true,
    autoplay: true,
  });
}

export function DotLottieAnimation({
  src,
  height,
  width,
  style,
  ...props
}: DotLottieAnimationProps): React.ReactNode {
  const ref = useRef<HTMLCanvasElement>(null);

  useIsomorphicLayoutEffect(() => {
    if (ref.current === null) {
      return;
    }

    const worker = createLottieWorker(ref.current, src);

    return () => {
      void worker.destroy();
    };
  }, [ref, src]);

  return <canvas ref={ref} style={{ height, width, ...style }} {...props} />;
}
lazybean commented 2 weeks ago

We get the same error just by enabling StrictMode in our nextjs app.

theashraf commented 2 weeks ago

@flow3d I haven't had time to look into this issue yet, but I'm happy to hear that you accept PRs if someone is interested in helping to fix it

alumag commented 3 days ago

Thanks to @lazybean comment I figured out it happens because createLottieWorker is called twice when StrictMode is enabled, causing a side-effect to run twice. I made this patch to make sure the effect runs only once. This will prevent re-rendering on any state changes, so be aware that this solution might not be suitable for you.

export function DotLottieAnimation({
  src,
  height,
  width,
  style,
  ...props
}: DotLottieAnimationProps): React.ReactNode {
  const ref = useRef<HTMLCanvasElement>(null);

  useIsomorphicLayoutEffect(() => {
    if (ref.current === null) {
      return;
    }

    const { current: canvas } = ref;

    // `createLottieWorker` has a side effect, so we have to force a single worker per canvas.
    // This is a workaround to prevent multiple workers from being created, which could happen
    // when react is on strict mode (development) or when the `src` prop changes.
    // It means that changing the props `src` will not cause re-render.
    if (canvas.hasAttribute("animation")) {
      return;
    }

    canvas.setAttribute("animation", "true");
    const worker = createLottieWorker(canvas, src);

    return () => {
      void worker.destroy();
    };
  }, [ref.current, src]);

  return <canvas ref={ref} style={{ height, width, ...style }} {...props} />;
}