Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: add useStateMachineInputs hook #310

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

Bokdol11859
Copy link

@Bokdol11859 Bokdol11859 commented Nov 27, 2024

Add new useStateMachineInputs Hook for fetching multiple stateMachine inputs from a rive file

Description

This pull request introduces a new hook, useStateMachineInputs, designed to easily get multiple inputs from a variable number of inputNames.

Previously, you had to map over the inputNames array to create input for each of them.
Since that behavior is against the rules of hooks, I decided to create this new hook.


This would be against the rules of hooks.
const inputs = inputNames.map(inputName => useStateMachineInput(rive, stateMachine, inputName));

With this hook, you can do it this way.
const inputs = useStateMachineInputs(rive, stateMachine, inputList);


Changes Made

CreateduseStateMachineInputs.ts

initialValue?: number | boolean;
}[]
) {
const [inputs, setInputs] = useState<StateMachineInput[] | null>(null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you think about removing the union with null and having inputs only be StateMachineInput[]?
I think an empty array is a simpler api to deal with.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that definitely seems like a better option. I'll change that!

Comment on lines 25 to 27
if (!rive || !stateMachineName || !inputNames) {
setInputs(null);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this necessary? It seems the next lines are checking the same and the else statement is also handling the alternate.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it doesn't seem necessary to me either, but I had that in because I found the same logic in the useStateMachineInput code.
I could create another pull request to refactor the useStateMachineInput hook if you're okay with it.
I'll work on removing some of the repeated logic.

}
setStateMachineInput();
if (rive) {
rive.on(EventType.Load, () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for safety, the useEffect hook can return a clean up function to clear this callback.
If not, multiple instances of the function might try to set the input values.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I'll add a rive.off() cleanup function.

@bodymovin
Copy link
Contributor

thanks for the PR! looks good overall, just left some comments on some details.

@lancesnider
Copy link
Contributor

Thanks for the PR! Right now this is how you'd use it, right?

const stateMachineInputs = useStateMachineInputs(rive, STATE_MACHINE_NAME, [
  { name: ON_HOVER_INPUT_NAME },
  { name: ON_PRESSED_INPUT_NAME },
]);

const onHoverInput = stateMachineInputs?.find(input => input.name === ON_HOVER_INPUT_NAME);
const onPressedInput = stateMachineInputs?.find(input => input.name === ON_PRESSED_INPUT_NAME);

I'd love to see it to where defining each input didn't require extra component code. Maybe something like:

const [onHoverInput, onPressedInput] = useStateMachineInputs(rive, STATE_MACHINE_NAME, [
  { name: ON_HOVER_INPUT_NAME },
  { name: ON_PRESSED_INPUT_NAME },
]);

@lancesnider
Copy link
Contributor

Does anyone else get this warning when using it in a component: useStateMachineInputs.ts:39 Warning: Maximum update depth exceeded

const selectedInputs = inputs.filter((input) =>
inputNames.some((inputName) => inputName.name === input.name)
);
if (selectedInputs) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this validation isn't needed, a filter call will always return an array at this point

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True! I'll fix that too!

@Bokdol11859
Copy link
Author

Thanks for the PR! Right now this is how you'd use it, right?

const stateMachineInputs = useStateMachineInputs(rive, STATE_MACHINE_NAME, [
  { name: ON_HOVER_INPUT_NAME },
  { name: ON_PRESSED_INPUT_NAME },
]);

const onHoverInput = stateMachineInputs?.find(input => input.name === ON_HOVER_INPUT_NAME);
const onPressedInput = stateMachineInputs?.find(input => input.name === ON_PRESSED_INPUT_NAME);

I'd love to see it to where defining each input didn't require extra component code. Maybe something like:

const [onHoverInput, onPressedInput] = useStateMachineInputs(rive, STATE_MACHINE_NAME, [
  { name: ON_HOVER_INPUT_NAME },
  { name: ON_PRESSED_INPUT_NAME },
]);

I just needed them to be fired all at once so I didn't have to manage them one by one.
But yeah that seems like a better approach to me too! I'll change that!

@Bokdol11859
Copy link
Author

Does anyone else get this warning when using it in a component: useStateMachineInputs.ts:39 Warning: Maximum update depth exceeded

I'll take a look at it!

@Bokdol11859
Copy link
Author

initialValue?: number | boolean;
}[]
) {
const [inputMap, setInputMap] = useState<Map<string, StateMachineInput>>(new Map());
Copy link
Contributor

@bodymovin bodymovin Nov 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a reason why the map needs to be stored in the state instead of the array?
It's great that it is being used as an intermediate store while building the inputs for performance, but that seems like an implementation detail of syncInputs.
In the end it's exposed as an array and It forces to use the useMemo hook.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope not at all anymore.
It was a Map for the initial implementation I planned, which is returning the Map instead of an Array. I thought it would be a better implementation as the user gets to choose which input they want to use, without having to map over the whole array again to find it.
But I realized that could be handled by preprocessing the inputNames array passed to the hook. ex) filtering out the unnecessary inputNames before calling the hook.

So yeah, there's no reason for it to be a Map instead of an Array now.
I'll change that! Thanks for pointing it out!

) {
const [inputMap, setInputMap] = useState<Map<string, StateMachineInput>>(new Map());

const syncInputs = useCallback(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

out of curiosity, in the previous implementation this method was defined in the useEffect hook directly. Was there a reason for moving it outside?
As far as I know, it's a good practice to declare methods that are only used in a useEffect, inside the useEffect hook itself. It helps with encapsulation. And in this particular case doesn't need the useCallback hook.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's true that declaring functions that are only used in a useEffect within the hook is helpful for encapsulation, but I think it kind of hurts the readability of the code in many cases.
I personally like to leave the useEffect hook as concise as possible so that I can understand the flow of the useEffects quickly without scrolling up and down too much.
(Besides, I find useEffects to be where the majority of unexpected bugs occur, so I try to keep them easy to understand in most cases.)
Since there's just one useEffect in this case, and since the function gets recreated whenever the useEffect runs (as they have the same dependency array), I don't really mind whether the function is defined inside or outside the hook.

If putting functions within useEffects is the convention Rive follows, I'm more than happy to move it back into the useEffect! Let me know what you think!

@Bokdol11859
Copy link
Author

Does anyone else get this warning when using it in a component: useStateMachineInputs.ts:39 Warning: Maximum update depth exceeded

@lancesnider Are you still seeing this warning? I don't seem to be getting it right now from where I'm using it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants