-
-
Notifications
You must be signed in to change notification settings - Fork 996
Description
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(...)
withMotionValue
— breaks declarative flow and requires more boilerplate. - Using
useSpring
with overridden settings to mimic othertype
— this feels like a hack and leads to unclear behavior.
Currently, it’s possible to override the animationtype
internally (e.g., setting{ type: "tween" }
insideuseSpring
), 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 becauseuseSpring
isn't typed to accept anything other thanspring
-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 ofTrailingCursorModule
.

- 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.

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.