Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
14 changes: 11 additions & 3 deletions src/hooks/useRive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
[]
Expand Down
35 changes: 34 additions & 1 deletion test/useRive.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' };

Expand Down
Loading