diff --git a/src/hooks/useRiveFile.ts b/src/hooks/useRiveFile.ts new file mode 100644 index 0000000..6ff9b4d --- /dev/null +++ b/src/hooks/useRiveFile.ts @@ -0,0 +1,38 @@ +import { useState, useEffect } from 'react'; +import type { UseRiveFileParameters } from '../types'; +import { RiveFile } from '@rive-app/canvas'; + +/** + * Custom hook for initializing and managing a RiveFile instance within a component. + * It sets up a RiveFile based on provided source parameters (URL or ArrayBuffer) and ensures + * proper cleanup to avoid memory leaks when the component unmounts or inputs change. + * + * @param params - Object containing parameters accepted by the Rive file in the rive-js runtime, + * + * @returns {RiveFile} Contains the active RiveFile instance (`riveFile`). + */ +function useRiveFile(params: UseRiveFileParameters) { + const [riveFile, setRiveFile] = useState(null); + + useEffect(() => { + let file: RiveFile | null = null; + + const loadRiveFile = async () => { + file = new RiveFile(params); + setRiveFile(file); + }; + + loadRiveFile(); + + return () => { + if (file) { + file.cleanup(); + } + }; + }, [params.src, params.buffer]); + + + return { riveFile }; +} + +export default useRiveFile; diff --git a/src/index.ts b/src/index.ts index d4c0d34..6ba9119 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,8 +2,9 @@ import Rive, { RiveProps } from './components/Rive'; import useRive from './hooks/useRive'; import useStateMachineInput from './hooks/useStateMachineInput'; import useResizeCanvas from './hooks/useResizeCanvas'; +import useRiveFile from './hooks/useRiveFile'; export default Rive; -export { useRive, useStateMachineInput, useResizeCanvas, RiveProps }; -export { RiveState, UseRiveParameters, UseRiveOptions } from './types'; +export { useRive, useStateMachineInput, useResizeCanvas, useRiveFile , RiveProps }; +export { RiveState, UseRiveParameters, UseRiveFileParameters, UseRiveOptions } from './types'; export * from '@rive-app/canvas'; diff --git a/src/types.ts b/src/types.ts index ea5a1c8..c7d9210 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,5 @@ import { RefCallback, ComponentProps } from 'react'; -import { Rive, RiveParameters } from '@rive-app/canvas'; +import { Rive, RiveParameters, RiveFileParameters } from '@rive-app/canvas'; export type UseRiveParameters = Partial> | null; @@ -36,3 +36,6 @@ export type RiveState = { rive: Rive | null; RiveComponent: (props: ComponentProps<'canvas'>) => JSX.Element; }; + + +export type UseRiveFileParameters = RiveFileParameters; \ No newline at end of file diff --git a/test/useRiveFile.test.tsx b/test/useRiveFile.test.tsx new file mode 100644 index 0000000..066de95 --- /dev/null +++ b/test/useRiveFile.test.tsx @@ -0,0 +1,95 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { mocked } from 'jest-mock'; + +import useRiveFile from '../src/hooks/useRiveFile'; +import { RiveFile } from '@rive-app/canvas'; + +jest.mock('@rive-app/canvas', () => ({ + RiveFile: jest.fn().mockImplementation(() => ({ + cleanup: jest.fn(), + })), +})); + + +describe('useRiveFile', () => { + beforeEach(() => { + mocked(RiveFile).mockClear(); + }); + + it('initializes RiveFile with provided parameters', async () => { + const params = { + src: 'file-src', + enableRiveAssetCDN: false + }; + + const { result } = renderHook(() => useRiveFile(params)); + + expect(RiveFile).toHaveBeenCalledWith(params); + expect(result.current.riveFile).toBeDefined(); + }); + + it('cleans up RiveFile on unmount', async () => { + const params = { + src: 'file-src', + enableRiveAssetCDN: false + }; + + const { result, unmount } = renderHook(() => useRiveFile(params)); + + const riveInstance = result.current.riveFile; + expect(riveInstance).toBeDefined(); + + unmount(); + + expect(riveInstance?.cleanup).toHaveBeenCalled(); + }); + + it('does not reinitialize RiveFile if params do not change', async () => { + const params = { + src: 'file-src', + enableRiveAssetCDN: false + }; + + const { rerender } = renderHook(() => useRiveFile(params)); + + rerender(); + + expect(RiveFile).toHaveBeenCalledTimes(1); + }); + + it('reinitializes RiveFile if src changes', async () => { + let params = { + src: 'file-src', + enableRiveAssetCDN: false + }; + + const { rerender } = renderHook(() => useRiveFile(params)); + + params = { + src: 'new-file-src', + enableRiveAssetCDN: false + }; + + rerender(); + + expect(RiveFile).toHaveBeenCalledTimes(2); + }); + + it('reinitializes RiveFile if buffer changes', async () => { + let params = { + buffer: new ArrayBuffer(10), + enableRiveAssetCDN: false + }; + + const { rerender } = renderHook(() => useRiveFile(params)); + + params = { + buffer: new ArrayBuffer(20), + enableRiveAssetCDN: false + }; + + rerender(); + + expect(RiveFile).toHaveBeenCalledTimes(2); + }); +});