Skip to content

[FEATURE] Add support for other animation types similar to useSpring, or create a generalized universal animation hook #3322

@Zh-end-oS

Description

@Zh-end-oS

Is your feature request related to a problem? Please describe.

Currently, useSpring is a powerful and flexible hook, but it's strictly tied to the spring animation type. There is no equivalent solution for other animation types such as "tween", "inertia" or "keyframes" that works in the same declarative way. This makes it harder to build generic animation hooks and breaks consistency.

Describe the solution you'd like

I suggest adding a generic hook (e.g., useMotion or useGenericMotion) that accepts the animation type in the options and returns a MotionValue:

const x = useGenericMotion(0, { type: "tween", duration: 0.3 });

Internally, it would use the correct animation type (spring, tween, inertia, etc.) depending on the options passed.

Describe alternatives you've considered

  • Creating separate hooks like useTween, useInertia, etc. — but this leads to duplicated logic.
  • Using imperative animate(...) with MotionValue — breaks declarative flow and requires more boilerplate.
  • Using useSpring with overridden settings to mimic other type — this feels like a hack and leads to unclear behavior.
    Currently, it’s possible to override the animation type internally (e.g., setting { type: "tween" } inside useSpring), but it's unclear how long this workaround will continue to work, as it’s not officially supported.
    While this hack technically works, it breaks type safety — TypeScript will raise errors because useSpring isn't typed to accept anything other than spring-related options.
    Taken together, this cannot be considered a reliable or future-proof solution.

Additional context

This hook would unify the usage of different animation types and make the API more consistent. It would also simplify declarative work with MotionValue without needing to manually invoke animate.


To evaluate performance, I created a chained animation module called TrailingCursorModule, which was sequentially mounted like this:

  // ...
  .use(muTrailingCursorModule)
  .use(muTrailingCursorModule)
  .use(muTrailingCursorModule)
  .use(muTrailingCursorModule)
  // ...

This mimics a heavy animation workload by stacking multiple trailing animations on the cursor.

Then, I simulated a 4x drop in system performance to observe rendering behavior and frame drops.


Code using useSpring:

    // ...
    const mouseX = useMotionValue(0);
    const mouseY = useMotionValue(0);
    const targetX = useTransform(mouseX, [-1, 1], amplitudesX);
    const targetY = useTransform(mouseY, [-1, 1], amplitudesY);
    const x = useSpring(targetX, currentAnimateOptions);
    const y = useSpring(targetY, currentAnimateOptions);
    // ...

Code using a manual animate setup (without useSpring):

    // ...
    const mouseX = useMotionValue(0);
    const mouseY = useMotionValue(0);
    const targetX = useTransform(mouseX, [-1, 1], amplitudesX);
    const targetY = useTransform(mouseY, [-1, 1], amplitudesY);
    const x = useMotionValue(0);
    const y = useMotionValue(0);

    useEffect(() => {
      const unsubX = targetX.on('change', (val) => {
        if (animationX.current) animationX.current.stop();
        animationX.current = animate(x, val, currentAnimateOptions);
      });

      const unsubY = targetY.on('change', (val) => {
        if (animationY.current) animationY.current.stop();
        animationY.current = animate(y, val, currentAnimateOptions);
      });

      return () => {
        unsubX();
        unsubY();
        if (animationX.current) animationX.current.stop();
        if (animationY.current) animationY.current.stop();
      };
    }, [targetX, targetY, x, y, currentAnimateOptions]);
    // ...

In both versions, currentAnimateOptions included { type: "tween" }.

Note:
useSpring automatically manages subscription cleanup, which is a definite advantage.
When using targetX.on('change', (val) => { ... }) directly, some developers may not realize the need to unsubscribe, which can lead to memory leaks or unintended behavior.
A universal hook could encapsulate this logic and ensure proper subscription management automatically.


Results:

  • With useSpring, the interaction performance under low system conditions was significantly better. The animation remained smooth and responsive, even with multiple instances of TrailingCursorModule.
Image
  • With the manual animate version, performance degraded noticeably under the same conditions — there was more stutter and frame drops, especially when multiple modules were chained.
Image

This demonstrates that useSpring is well-optimized for performance. However, currently one has to either use useSpring with overridden internal behavior (e.g., by passing { type: 'tween' }), or implement a custom animation hook similar to the library’s core code. While overriding works for now, it is an undocumented and weakly-typed workaround that may break in future versions.

Providing dedicated hooks like useTween, useDecay, etc., or a generalized useAnimateValue hook that internally routes to the correct animation type, would not only clarify developer intent and reduce misuse, but also lead to better maintainability, cleaner code, and more predictable performance in production.

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions