From ec8c4eb9154a41ed9a10b2b2e0ee9e20ae947569 Mon Sep 17 00:00:00 2001 From: bokdol11859 <2019147551@yonsei.ac.kr> Date: Wed, 27 Nov 2024 18:32:05 +0900 Subject: [PATCH 1/8] Feat: add useStateMachineInputs hook --- src/hooks/useStateMachineInputs.ts | 60 ++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/hooks/useStateMachineInputs.ts diff --git a/src/hooks/useStateMachineInputs.ts b/src/hooks/useStateMachineInputs.ts new file mode 100644 index 00000000..a13408c5 --- /dev/null +++ b/src/hooks/useStateMachineInputs.ts @@ -0,0 +1,60 @@ +import { useState, useEffect } from 'react'; +import { EventType, Rive, StateMachineInput } from '@rive-app/canvas'; + +/** + * Custom hook for fetching multiple stateMachine inputs from a rive file. + * Particularly useful for fetching multiple inputs from a variable number of input names. + * + * @param rive - Rive instance + * @param stateMachineName - Name of the state machine + * @param inputNames - Names of the inputs + * @returns StateMachineInput[] | null + */ +export default function useStateMachineInputs( + rive: Rive | null, + stateMachineName?: string, + inputNames?: { + name: string; + initialValue?: number | boolean; + }[] +) { + const [inputs, setInputs] = useState(null); + + useEffect(() => { + function setStateMachineInput() { + if (!rive || !stateMachineName || !inputNames) { + setInputs(null); + } + + if (rive && stateMachineName && inputNames) { + const inputs = rive.stateMachineInputs(stateMachineName); + if (inputs) { + const selectedInputs = inputs.filter((input) => + inputNames.some((inputName) => inputName.name === input.name) + ); + if (selectedInputs) { + selectedInputs.forEach((input) => { + const targetInputName = inputNames.find(inputName => inputName.name === input.name); + if(targetInputName?.initialValue){ + input.value = targetInputName.initialValue + } + }); + } + setInputs(selectedInputs); + } + } else { + setInputs(null); + } + } + setStateMachineInput(); + if (rive) { + rive.on(EventType.Load, () => { + // Check if the component/canvas is mounted before setting state to avoid setState + // on an unmounted component in some rare cases + setStateMachineInput(); + }); + } + }, [inputNames, rive, stateMachineName]); + + return inputs; +} From fa9e7deac49f251d0e2a6b2fadb33acc0133309a Mon Sep 17 00:00:00 2001 From: bokdol11859 <2019147551@yonsei.ac.kr> Date: Wed, 27 Nov 2024 18:49:11 +0900 Subject: [PATCH 2/8] feat: add useStateMachineInputs to exports --- src/index.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 6ba9119c..1f913e60 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,8 +3,22 @@ import useRive from './hooks/useRive'; import useStateMachineInput from './hooks/useStateMachineInput'; import useResizeCanvas from './hooks/useResizeCanvas'; import useRiveFile from './hooks/useRiveFile'; +import useStateMachineInputs from './hooks/useStateMachineInputs'; export default Rive; -export { useRive, useStateMachineInput, useResizeCanvas, useRiveFile , RiveProps }; -export { RiveState, UseRiveParameters, UseRiveFileParameters, UseRiveOptions } from './types'; +export { + useRive, + useStateMachineInput, + useStateMachineInputs, + useResizeCanvas, + useRiveFile, + RiveProps, +}; +export { + RiveState, + UseRiveParameters, + UseRiveFileParameters, + UseRiveOptions, +} from './types'; + export * from '@rive-app/canvas'; From 2a26db1f0337591498543cf7aabc81e46457fcef Mon Sep 17 00:00:00 2001 From: bokdol11859 <2019147551@yonsei.ac.kr> Date: Thu, 28 Nov 2024 14:53:18 +0900 Subject: [PATCH 3/8] feat: update useStateMachineInputs hook with optimizations --- src/hooks/useStateMachineInputs.ts | 79 ++++++++++++++++-------------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/src/hooks/useStateMachineInputs.ts b/src/hooks/useStateMachineInputs.ts index a13408c5..530f8c4e 100644 --- a/src/hooks/useStateMachineInputs.ts +++ b/src/hooks/useStateMachineInputs.ts @@ -1,5 +1,5 @@ -import { useState, useEffect } from 'react'; -import { EventType, Rive, StateMachineInput } from '@rive-app/canvas'; +import { EventType, StateMachineInput, Rive } from '@rive-app/canvas'; +import { useCallback, useEffect, useMemo, useState } from 'react'; /** * Custom hook for fetching multiple stateMachine inputs from a rive file. @@ -7,8 +7,8 @@ import { EventType, Rive, StateMachineInput } from '@rive-app/canvas'; * * @param rive - Rive instance * @param stateMachineName - Name of the state machine - * @param inputNames - Names of the inputs - * @returns StateMachineInput[] | null + * @param inputNames - Name and initial value of the inputs + * @returns StateMachineInput[] */ export default function useStateMachineInputs( rive: Rive | null, @@ -18,43 +18,46 @@ export default function useStateMachineInputs( initialValue?: number | boolean; }[] ) { - const [inputs, setInputs] = useState(null); + const [inputMap, setInputMap] = useState>(new Map()); - useEffect(() => { - function setStateMachineInput() { - if (!rive || !stateMachineName || !inputNames) { - setInputs(null); - } - - if (rive && stateMachineName && inputNames) { - const inputs = rive.stateMachineInputs(stateMachineName); - if (inputs) { - const selectedInputs = inputs.filter((input) => - inputNames.some((inputName) => inputName.name === input.name) - ); - if (selectedInputs) { - selectedInputs.forEach((input) => { - const targetInputName = inputNames.find(inputName => inputName.name === input.name); - if(targetInputName?.initialValue){ - input.value = targetInputName.initialValue - } - }); - } - setInputs(selectedInputs); + const syncInputs = useCallback(() => { + if (!rive || !stateMachineName || !inputNames) return; + + const riveInputs = rive.stateMachineInputs(stateMachineName); + if (!riveInputs) return; + + // To optimize lookup time from O(n) to O(1) in the following loop + const riveInputLookup = new Map(riveInputs.map(input => [input.name, input])); + + setInputMap(() => { + const newMap = new Map(); + + // Iterate over inputNames instead of riveInputs to preserve array order + inputNames.forEach(inputName => { + const riveInput = riveInputLookup.get(inputName.name); + if (!riveInput) return; + + if (inputName.initialValue !== undefined) { + riveInput.value = inputName.initialValue; } - } else { - setInputs(null); - } - } - setStateMachineInput(); - if (rive) { - rive.on(EventType.Load, () => { - // Check if the component/canvas is mounted before setting state to avoid setState - // on an unmounted component in some rare cases - setStateMachineInput(); + + newMap.set(inputName.name, riveInput); }); - } + + return newMap; + }); }, [inputNames, rive, stateMachineName]); - return inputs; + useEffect(() => { + syncInputs(); + if (rive) { + rive.on(EventType.Load, syncInputs); + + return () => { + rive.off(EventType.Load, syncInputs); + }; + } + }, [rive, stateMachineName, inputNames, syncInputs]); + + return useMemo(() => Array.from(inputMap.values()), [inputMap]); } From fc0c547148a5208b9c2ca86038cc3688d9e7ed19 Mon Sep 17 00:00:00 2001 From: bokdol11859 <2019147551@yonsei.ac.kr> Date: Fri, 29 Nov 2024 09:47:54 +0900 Subject: [PATCH 4/8] refactor: change state type from Map to Array to remove unnecessary transformation --- src/hooks/useStateMachineInputs.ts | 33 +++++++++++++++--------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/hooks/useStateMachineInputs.ts b/src/hooks/useStateMachineInputs.ts index 530f8c4e..476c7206 100644 --- a/src/hooks/useStateMachineInputs.ts +++ b/src/hooks/useStateMachineInputs.ts @@ -1,5 +1,5 @@ import { EventType, StateMachineInput, Rive } from '@rive-app/canvas'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; /** * Custom hook for fetching multiple stateMachine inputs from a rive file. @@ -18,7 +18,7 @@ export default function useStateMachineInputs( initialValue?: number | boolean; }[] ) { - const [inputMap, setInputMap] = useState>(new Map()); + const [inputs, setInputs] = useState([]); const syncInputs = useCallback(() => { if (!rive || !stateMachineName || !inputNames) return; @@ -27,24 +27,23 @@ export default function useStateMachineInputs( if (!riveInputs) return; // To optimize lookup time from O(n) to O(1) in the following loop - const riveInputLookup = new Map(riveInputs.map(input => [input.name, input])); - - setInputMap(() => { - const newMap = new Map(); + const riveInputLookup = new Map( + riveInputs.map(input => [input.name, input]) + ); + setInputs(() => { // Iterate over inputNames instead of riveInputs to preserve array order - inputNames.forEach(inputName => { - const riveInput = riveInputLookup.get(inputName.name); - if (!riveInput) return; - - if (inputName.initialValue !== undefined) { - riveInput.value = inputName.initialValue; - } + return inputNames + .filter(inputName => riveInputLookup.has(inputName.name)) + .map(inputName => { + const riveInput = riveInputLookup.get(inputName.name)!; - newMap.set(inputName.name, riveInput); - }); + if (inputName.initialValue !== undefined) { + riveInput.value = inputName.initialValue; + } - return newMap; + return riveInput; + }); }); }, [inputNames, rive, stateMachineName]); @@ -59,5 +58,5 @@ export default function useStateMachineInputs( } }, [rive, stateMachineName, inputNames, syncInputs]); - return useMemo(() => Array.from(inputMap.values()), [inputMap]); + return inputs; } From f25dc49b60f8a9d38df8441a6697f702f50661af Mon Sep 17 00:00:00 2001 From: bokdol11859 <2019147551@yonsei.ac.kr> Date: Wed, 4 Dec 2024 17:24:12 +0900 Subject: [PATCH 5/8] fix: fix "Maximum update depth exceeded" error --- src/hooks/useStateMachineInputs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useStateMachineInputs.ts b/src/hooks/useStateMachineInputs.ts index 476c7206..0f2d7f52 100644 --- a/src/hooks/useStateMachineInputs.ts +++ b/src/hooks/useStateMachineInputs.ts @@ -56,7 +56,7 @@ export default function useStateMachineInputs( rive.off(EventType.Load, syncInputs); }; } - }, [rive, stateMachineName, inputNames, syncInputs]); + }, [rive]); return inputs; } From 79ca8487909b67485942b10d8bbe7fcf4cf064e7 Mon Sep 17 00:00:00 2001 From: bokdol11859 <2019147551@yonsei.ac.kr> Date: Wed, 4 Dec 2024 17:24:36 +0900 Subject: [PATCH 6/8] test: add test cases for useStateMachineInputs hook --- test/useStateMachineInputs.test.tsx | 161 ++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 test/useStateMachineInputs.test.tsx diff --git a/test/useStateMachineInputs.test.tsx b/test/useStateMachineInputs.test.tsx new file mode 100644 index 00000000..c968ec00 --- /dev/null +++ b/test/useStateMachineInputs.test.tsx @@ -0,0 +1,161 @@ +import { mocked } from 'jest-mock'; +import { renderHook } from '@testing-library/react-hooks'; + +import useStateMachineInputs from '../src/hooks/useStateMachineInputs'; +import { Rive, StateMachineInput } from '@rive-app/canvas'; + +jest.mock('@rive-app/canvas', () => ({ + Rive: jest.fn().mockImplementation(() => ({ + on: jest.fn(), + off: jest.fn(), + stop: jest.fn(), + stateMachineInputs: jest.fn(), + })), + Layout: jest.fn(), + Fit: { + Cover: 'cover', + }, + Alignment: { + Center: 'center', + }, + EventType: { + Load: 'load', + }, + StateMachineInputType: { + Number: 1, + Boolean: 2, + Trigger: 3, + }, +})); + +function getRiveMock({ + smiInputs, +}: { + smiInputs?: null | StateMachineInput[]; +} = {}) { + const riveMock = new Rive({ + canvas: undefined as unknown as HTMLCanvasElement, + }); + if (smiInputs) { + riveMock.stateMachineInputs = jest.fn().mockReturnValue(smiInputs); + } + + return riveMock; +} + +describe('useStateMachineInputs', () => { + it('returns empty array if there is null rive object passed', () => { + const { result } = renderHook(() => useStateMachineInputs(null)); + expect(result.current).toEqual([]); + }); + + it('returns empty array if there is no state machine name', () => { + const riveMock = getRiveMock(); + mocked(Rive).mockImplementation(() => riveMock); + + const { result } = renderHook(() => + useStateMachineInputs(riveMock, '', [{ name: 'testInput' }]) + ); + expect(result.current).toEqual([]); + }); + + it('returns empty array if there are no input names provided', () => { + const riveMock = getRiveMock(); + mocked(Rive).mockImplementation(() => riveMock); + + const { result } = renderHook(() => + useStateMachineInputs(riveMock, 'smName', []) + ); + expect(result.current).toEqual([]); + }); + + it('returns empty array if there are no inputs for the state machine', () => { + const riveMock = getRiveMock({ smiInputs: [] }); + mocked(Rive).mockImplementation(() => riveMock); + + const { result } = renderHook(() => + useStateMachineInputs(riveMock, 'smName', [{ name: 'testInput' }]) + ); + expect(result.current).toEqual([]); + }); + + it('returns only the inputs that exist in the state machine', () => { + const smInputs = [ + { name: 'input1' } as StateMachineInput, + { name: 'input2' } as StateMachineInput, + ]; + const riveMock = getRiveMock({ smiInputs: smInputs }); + mocked(Rive).mockImplementation(() => riveMock); + + const { result } = renderHook(() => + useStateMachineInputs(riveMock, 'smName', [ + { name: 'input1' }, + { name: 'nonexistent' }, + { name: 'input2' }, + ]) + ); + expect(result.current).toEqual([smInputs[0], smInputs[1]]); + }); + + it('sets initial values on the inputs when provided', () => { + const smInputs = [ + { name: 'boolInput', value: false } as StateMachineInput, + { name: 'numInput', value: 0 } as StateMachineInput, + ]; + const riveMock = getRiveMock({ smiInputs: smInputs }); + mocked(Rive).mockImplementation(() => riveMock); + + const { result } = renderHook(() => + useStateMachineInputs(riveMock, 'smName', [ + { name: 'boolInput', initialValue: true }, + { name: 'numInput', initialValue: 42 }, + ]) + ); + + expect(result.current[0].value).toBe(true); + expect(result.current[1].value).toBe(42); + }); + + it('does not set initial values if not provided', () => { + const smInputs = [ + { name: 'boolInput', value: false } as StateMachineInput, + { name: 'numInput', value: 0 } as StateMachineInput, + ]; + const riveMock = getRiveMock({ smiInputs: smInputs }); + mocked(Rive).mockImplementation(() => riveMock); + + const { result } = renderHook(() => + useStateMachineInputs(riveMock, 'smName', [ + { name: 'boolInput' }, + { name: 'numInput' }, + ]) + ); + + expect(result.current[0].value).toBe(false); + expect(result.current[1].value).toBe(0); + }); + + it('preserves the order of inputs as specified in inputNames', () => { + const smInputs = [ + { name: 'input1' } as StateMachineInput, + { name: 'input2' } as StateMachineInput, + { name: 'input3' } as StateMachineInput, + ]; + const riveMock = getRiveMock({ smiInputs: smInputs }); + mocked(Rive).mockImplementation(() => riveMock); + + const { result } = renderHook(() => + useStateMachineInputs(riveMock, 'smName', [ + { name: 'input3' }, + { name: 'input1' }, + { name: 'input2' }, + ]) + ); + + expect(result.current.map((input) => input.name)).toEqual([ + 'input3', + 'input1', + 'input2', + ]); + }); +}); From 0fc363e7e1f1641acd9a488c6b7f85f850a6978c Mon Sep 17 00:00:00 2001 From: bokdol11859 <2019147551@yonsei.ac.kr> Date: Thu, 2 Jan 2025 22:26:16 +0900 Subject: [PATCH 7/8] refactor: move syncInputs function inside useEffect --- src/hooks/useStateMachineInputs.ts | 44 +++++++++++++++--------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/hooks/useStateMachineInputs.ts b/src/hooks/useStateMachineInputs.ts index 0f2d7f52..b1016111 100644 --- a/src/hooks/useStateMachineInputs.ts +++ b/src/hooks/useStateMachineInputs.ts @@ -20,34 +20,34 @@ export default function useStateMachineInputs( ) { const [inputs, setInputs] = useState([]); - const syncInputs = useCallback(() => { - if (!rive || !stateMachineName || !inputNames) return; + useEffect(() => { + const syncInputs = () => { + if (!rive || !stateMachineName || !inputNames) return; - const riveInputs = rive.stateMachineInputs(stateMachineName); - if (!riveInputs) return; + const riveInputs = rive.stateMachineInputs(stateMachineName); + if (!riveInputs) return; - // To optimize lookup time from O(n) to O(1) in the following loop - const riveInputLookup = new Map( - riveInputs.map(input => [input.name, input]) - ); + // To optimize lookup time from O(n) to O(1) in the following loop + const riveInputLookup = new Map( + riveInputs.map(input => [input.name, input]) + ); - setInputs(() => { - // Iterate over inputNames instead of riveInputs to preserve array order - return inputNames - .filter(inputName => riveInputLookup.has(inputName.name)) - .map(inputName => { - const riveInput = riveInputLookup.get(inputName.name)!; + setInputs(() => { + // Iterate over inputNames instead of riveInputs to preserve array order + return inputNames + .filter(inputName => riveInputLookup.has(inputName.name)) + .map(inputName => { + const riveInput = riveInputLookup.get(inputName.name)!; - if (inputName.initialValue !== undefined) { - riveInput.value = inputName.initialValue; - } + if (inputName.initialValue !== undefined) { + riveInput.value = inputName.initialValue; + } - return riveInput; - }); - }); - }, [inputNames, rive, stateMachineName]); + return riveInput; + }); + }); + }; - useEffect(() => { syncInputs(); if (rive) { rive.on(EventType.Load, syncInputs); From 9a7b3eea2d85b038183b8d421b736fd004085123 Mon Sep 17 00:00:00 2001 From: bokdol11859 <2019147551@yonsei.ac.kr> Date: Tue, 7 Jan 2025 18:02:54 +0900 Subject: [PATCH 8/8] chore: remove unused import --- src/hooks/useStateMachineInputs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useStateMachineInputs.ts b/src/hooks/useStateMachineInputs.ts index b1016111..07ea329b 100644 --- a/src/hooks/useStateMachineInputs.ts +++ b/src/hooks/useStateMachineInputs.ts @@ -1,5 +1,5 @@ import { EventType, StateMachineInput, Rive } from '@rive-app/canvas'; -import { useCallback, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; /** * Custom hook for fetching multiple stateMachine inputs from a rive file.