diff --git a/package-lock.json b/package-lock.json index 088a6a0a..ac7f2abe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,9 @@ "version": "4.34.1", "license": "MIT", "dependencies": { - "@rive-app/canvas": "2.42.0", - "@rive-app/canvas-lite": "2.42.0", - "@rive-app/webgl2": "2.42.0" + "@rive-app/canvas": "2.42.1", + "@rive-app/canvas-lite": "2.42.1", + "@rive-app/webgl2": "2.42.1" }, "devDependencies": { "@babel/core": "^7.18.0", @@ -1357,21 +1357,21 @@ } }, "node_modules/@rive-app/canvas": { - "version": "2.42.0", - "resolved": "https://registry.npmjs.org/@rive-app/canvas/-/canvas-2.42.0.tgz", - "integrity": "sha512-ByXG7i8PZGFhPDUA9tkjEbB/C1cjs775IxFdPrWiQDN/WYMU6MirI2x0kGjHHwdfUKuG2HSkWEUI5SBeZm/hEA==", + "version": "2.42.1", + "resolved": "https://registry.npmjs.org/@rive-app/canvas/-/canvas-2.42.1.tgz", + "integrity": "sha512-S9u3DNWWTmACjlq5vW0KmG6IpMRVx+JNExdJskAhMulEP7rC64LPufQf6vCuL9g1ogbrgT828vFEyraRVJn8fA==", "license": "MIT" }, "node_modules/@rive-app/canvas-lite": { - "version": "2.42.0", - "resolved": "https://registry.npmjs.org/@rive-app/canvas-lite/-/canvas-lite-2.42.0.tgz", - "integrity": "sha512-U3X0AJxOQFMbBYg0Svb4cmln8SJpOQLm2c54sCj/iMo4liiw15GKQbTChQrGJwEqrGkjS+5R5ZOdVqHPgg964A==", + "version": "2.42.1", + "resolved": "https://registry.npmjs.org/@rive-app/canvas-lite/-/canvas-lite-2.42.1.tgz", + "integrity": "sha512-pBmcP4WJV0QY851H27LI4ncWnORo8BA9YhoOUb9lsD5rPdlCIMR3cMkE7VcZ0f8Km+tg64+96vrexnkRieJntw==", "license": "MIT" }, "node_modules/@rive-app/webgl2": { - "version": "2.42.0", - "resolved": "https://registry.npmjs.org/@rive-app/webgl2/-/webgl2-2.42.0.tgz", - "integrity": "sha512-l5KJsxSc39lirk5+EHKKwkEdV84vI14zc7N7B5jBdVAk3E4Tg79C0gd5JmScoJaoRkc6wi6X952KbALji+NQZg==", + "version": "2.42.1", + "resolved": "https://registry.npmjs.org/@rive-app/webgl2/-/webgl2-2.42.1.tgz", + "integrity": "sha512-yOyznVtUTPAew6bsJBHbLmOaUwheWMHHsAXmbI8h6MdnwMxpOqQrP00BuZAHlC86ib5ubUlSBZfzjL84sn+Rsg==", "license": "MIT" }, "node_modules/@rollup/plugin-commonjs": { diff --git a/package.json b/package.json index 64916f6f..14dca370 100644 --- a/package.json +++ b/package.json @@ -35,9 +35,9 @@ }, "homepage": "https://github.com/rive-app/rive-react#readme", "dependencies": { - "@rive-app/canvas": "2.42.0", - "@rive-app/canvas-lite": "2.42.0", - "@rive-app/webgl2": "2.42.0" + "@rive-app/canvas": "2.42.1", + "@rive-app/canvas-lite": "2.42.1", + "@rive-app/webgl2": "2.42.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0" diff --git a/src/hooks/useRive.tsx b/src/hooks/useRive.tsx index 8049b4f3..78cdd295 100644 --- a/src/hooks/useRive.tsx +++ b/src/hooks/useRive.tsx @@ -159,12 +159,20 @@ export default function useRive( // frees it synchronously. Read the previous canvas from a ref, because // canvasElem is never updated in this callback const previousCanvas = canvasRef.current; + canvasRef.current = canvas; + if (canvas === null && previousCanvas) { - previousCanvas.height = 0; - previousCanvas.width = 0; + // A null ref is not always an unmount. React detaches and re-attaches + // the same element when it re-runs a mount on strict mode -- so + // defer and release only if this element did not come back. + queueMicrotask(() => { + if (canvasRef.current !== previousCanvas) { + previousCanvas.height = 0; + previousCanvas.width = 0; + } + }); } - canvasRef.current = canvas; setCanvasElem(canvas); }, [] diff --git a/test/useRive.test.tsx b/test/useRive.test.tsx index 06512ccd..9c140adb 100644 --- a/test/useRive.test.tsx +++ b/test/useRive.test.tsx @@ -455,12 +455,45 @@ describe('useRive', () => { expect(captured!.width).toBe(800); expect(captured!.height).toBe(600); - unmount(); + // The release is deferred a microtask so a ref detach that is immediately + // followed by a re-attach (StrictMode) does not zero a live canvas. + await act(async () => { + unmount(); + }); expect(captured!.width).toBe(0); expect(captured!.height).toBe(0); }); + it('does not release the backing store when the ref is re-attached immediately', async () => { + // React re-runs a mount by detaching the ref and immediately re-attaching + // the identical element; StrictMode does this on every mount under React 19. + const params = { src: 'file-src' }; + + // @ts-ignore + mocked(rive.Rive).mockImplementation(() => baseRiveMock); + + const canvasSpy = document.createElement('canvas'); + const { result } = renderHook(() => useRive(params)); + + await act(async () => { + result.current.setCanvasRef(canvasSpy); + }); + await waitFor(() => expect(result.current.canvas).toBe(canvasSpy)); + + canvasSpy.width = 800; + canvasSpy.height = 600; + + await act(async () => { + result.current.setCanvasRef(null); + result.current.setCanvasRef(canvasSpy); + await Promise.resolve(); + }); + + expect(canvasSpy.width).toBe(800); + expect(canvasSpy.height).toBe(600); + }); + it('keeps setCanvasRef referentially stable across renders', async () => { const params = { src: 'file-src' };