diff --git a/.changeset/fast-garlics-retire.md b/.changeset/fast-garlics-retire.md new file mode 100644 index 00000000..a0f9e2fe --- /dev/null +++ b/.changeset/fast-garlics-retire.md @@ -0,0 +1,5 @@ +--- +'@lottiefiles/dotlottie-react': patch +--- + +fix(react): 🐛 dotlottieRefCallback null instance in strict mode diff --git a/apps/dotlottie-react-example/src/client-entry.tsx b/apps/dotlottie-react-example/src/client-entry.tsx index d53e437b..33356ea1 100644 --- a/apps/dotlottie-react-example/src/client-entry.tsx +++ b/apps/dotlottie-react-example/src/client-entry.tsx @@ -1,10 +1,11 @@ import './index.css'; +import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; ReactDOM.hydrateRoot( document.getElementById('root') as HTMLElement, - // - , - // , + + , + , ); diff --git a/apps/dotlottie-react-example/src/server-entry.tsx b/apps/dotlottie-react-example/src/server-entry.tsx index 3ade5857..58f5dd6d 100644 --- a/apps/dotlottie-react-example/src/server-entry.tsx +++ b/apps/dotlottie-react-example/src/server-entry.tsx @@ -1,12 +1,13 @@ import './index.css'; +import React from 'react'; import ReactDOMServer from 'react-dom/server'; import App from './App'; export function render() { const html = ReactDOMServer.renderToString( - // - , - // , + + , + , ); return { html }; } diff --git a/packages/react/src/use-dotlottie.tsx b/packages/react/src/use-dotlottie.tsx index 05215acf..6156ed46 100644 --- a/packages/react/src/use-dotlottie.tsx +++ b/packages/react/src/use-dotlottie.tsx @@ -107,46 +107,9 @@ export const useDotLottie = (config?: DotLottieConfig): UseDotLottieResult => { return new ResizeObserver(observerCallback); }, []); - const setCanvasRef = useCallback( - (canvas: HTMLCanvasElement | null) => { - if (canvas) { - const dotLottieInstance = new DotLottie({ - ...config, - canvas, - }); - - setDotLottie(dotLottieInstance); - - // Check if the canvas is initially in view - const initialEntry = canvas.getBoundingClientRect(); - - if ( - initialEntry.top >= 0 && - initialEntry.left >= 0 && - initialEntry.bottom <= (window.innerHeight || document.documentElement.clientHeight) && - initialEntry.right <= (window.innerWidth || document.documentElement.clientWidth) - ) { - dotLottieInstance.unfreeze(); - } else { - dotLottieInstance.freeze(); - } - - intersectionObserver?.observe(canvas); - if (config?.autoResizeCanvas) { - resizeObserver?.observe(canvas); - } - canvas.addEventListener('mouseenter', hoverHandler); - canvas.addEventListener('mouseleave', hoverHandler); - } else { - dotLottieRef.current?.destroy(); - intersectionObserver?.disconnect(); - resizeObserver?.disconnect(); - } - - canvasRef.current = canvas; - }, - [intersectionObserver, resizeObserver, hoverHandler], - ); + const setCanvasRef = useCallback((canvas: HTMLCanvasElement | null) => { + canvasRef.current = canvas; + }, []); const setContainerRef = useCallback((container: HTMLDivElement | null) => { containerRef.current = container; @@ -160,98 +123,134 @@ export const useDotLottie = (config?: DotLottieConfig): UseDotLottieResult => { ); useEffect(() => { + const canvas = canvasRef.current; + + let dotLottieInstance: DotLottie | null = null; + + if (canvas) { + dotLottieInstance = new DotLottie({ + ...configRef.current, + canvas, + }); + + // Check if the canvas is initially in view + const initialEntry = canvas.getBoundingClientRect(); + + if ( + initialEntry.top >= 0 && + initialEntry.left >= 0 && + initialEntry.bottom <= (window.innerHeight || document.documentElement.clientHeight) && + initialEntry.right <= (window.innerWidth || document.documentElement.clientWidth) + ) { + dotLottieInstance.unfreeze(); + } else { + dotLottieInstance.freeze(); + } + + intersectionObserver?.observe(canvas); + + if (config?.autoResizeCanvas) { + resizeObserver?.observe(canvas); + } + canvas.addEventListener('mouseenter', hoverHandler); + canvas.addEventListener('mouseleave', hoverHandler); + + setDotLottie(dotLottieInstance); + } + return () => { - dotLottie?.destroy(); + dotLottieInstance?.destroy(); setDotLottie(null); resizeObserver?.disconnect(); intersectionObserver?.disconnect(); - canvasRef.current?.removeEventListener('mouseenter', hoverHandler); - canvasRef.current?.removeEventListener('mouseleave', hoverHandler); + canvas?.removeEventListener('mouseenter', hoverHandler); + canvas?.removeEventListener('mouseleave', hoverHandler); }; - }, []); + }, [intersectionObserver, resizeObserver, hoverHandler]); // speed reactivity useEffect(() => { - if (!dotLottie) return; + if (!dotLottieRef.current) return; - if (typeof config?.speed === 'number' && config.speed !== dotLottie.speed && dotLottie.isLoaded) { - dotLottie.setSpeed(config.speed); + if (typeof config?.speed === 'number' && config.speed !== dotLottieRef.current.speed) { + dotLottieRef.current.setSpeed(config.speed); } }, [config?.speed]); // mode reactivity useEffect(() => { - if (!dotLottie) return; + if (!dotLottieRef.current) return; - if (typeof config?.mode === 'string' && config.mode !== dotLottie.mode && dotLottie.isLoaded) { - dotLottie.setMode(config.mode); + if (typeof config?.mode === 'string' && config.mode !== dotLottieRef.current.mode) { + dotLottieRef.current.setMode(config.mode); } }, [config?.mode]); // loop reactivity useEffect(() => { - if (!dotLottie) return; + if (!dotLottieRef.current) return; - if (typeof config?.loop === 'boolean' && config.loop !== dotLottie.loop && dotLottie.isLoaded) { - dotLottie.setLoop(config.loop); + if (typeof config?.loop === 'boolean' && config.loop !== dotLottieRef.current.loop) { + dotLottieRef.current.setLoop(config.loop); } }, [config?.loop]); // useFrameInterpolation reactivity useEffect(() => { - if (!dotLottie) return; + if (!dotLottieRef.current) return; if ( typeof config?.useFrameInterpolation === 'boolean' && - config.useFrameInterpolation !== dotLottie.useFrameInterpolation && - dotLottie.isLoaded + config.useFrameInterpolation !== dotLottieRef.current.useFrameInterpolation ) { - dotLottie.setUseFrameInterpolation(config.useFrameInterpolation); + dotLottieRef.current.setUseFrameInterpolation(config.useFrameInterpolation); } }, [config?.useFrameInterpolation]); // segment reactivity useEffect(() => { - if (!dotLottie) return; + if (!dotLottieRef.current) return; if ( typeof config?.segment === 'object' && Array.isArray(config.segment) && // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - config.segment.length === 2 && - dotLottie.isLoaded + config.segment.length === 2 ) { const startFrame = config.segment[0]; const endFrame = config.segment[1]; - dotLottie.setSegment(startFrame, endFrame); + dotLottieRef.current.setSegment(startFrame, endFrame); } }, [config?.segment]); // background color reactivity useEffect(() => { - if (!dotLottie) return; + if (!dotLottieRef.current) return; - if (typeof config?.backgroundColor === 'string' && config.backgroundColor !== dotLottie.backgroundColor) { - dotLottie.setBackgroundColor(config.backgroundColor); + if ( + typeof config?.backgroundColor === 'string' && + config.backgroundColor !== dotLottieRef.current.backgroundColor + ) { + dotLottieRef.current.setBackgroundColor(config.backgroundColor); } }, [config?.backgroundColor]); // render config reactivity useEffect(() => { - if (!dotLottie) return; + if (!dotLottieRef.current) return; if (typeof config?.renderConfig === 'object') { - dotLottie.setRenderConfig(config.renderConfig); + dotLottieRef.current.setRenderConfig(config.renderConfig); } - }, [config?.renderConfig]); + }, [JSON.stringify(config?.renderConfig)]); // data reactivity useEffect(() => { - if (!dotLottie) return; + if (!dotLottieRef.current) return; if (typeof config?.data === 'string' || config?.data instanceof ArrayBuffer) { - dotLottie.load({ + dotLottieRef.current.load({ data: config.data, ...(configRef.current || {}), }); @@ -260,10 +259,10 @@ export const useDotLottie = (config?: DotLottieConfig): UseDotLottieResult => { // src reactivity useEffect(() => { - if (!dotLottie) return; + if (!dotLottieRef.current) return; if (typeof config?.src === 'string') { - dotLottie.load({ + dotLottieRef.current.load({ src: config.src, ...(configRef.current || {}), }); @@ -271,10 +270,10 @@ export const useDotLottie = (config?: DotLottieConfig): UseDotLottieResult => { }, [config?.src]); useEffect(() => { - if (!dotLottie) return; + if (!dotLottieRef.current) return; if (typeof config?.marker === 'string') { - dotLottie.setMarker(config.marker); + dotLottieRef.current.setMarker(config.marker); } }, [config?.marker]);