I'm trying to use useSpring to trigger an animation when an EventEmitter emits an animation event. The code looks like this.
export const useAnimationListener = () => {
const [animation, setAnimation] = useState({});
const style = useSpring(animation);
function animate(event) {
setAnimation({
opacity: 1,
from: {opacity: 0},
reset: true,
});
}
useEffect(() => {
AnimationDispatcher.on('animate', animate);
return function cleanup() {
AnimationDispatcher.removeListener('animate', animate);
};
});
console.log('style', style);
return style;
};
This works fine for every animation triggered after the first animation. The first animation doesn't fire, and if I inspect the animation state value, and the subsequent style value returned by useSpring, I see the expected animation parameters, but an empty animated style object.
This can be alleviated in one of two ways. 1) I can populate animation like so: const [animation, setAnimation] = useState({opacity: 1, from: {opacity: 1}}), in which case the first animation fires as expected. 2) I can call setAnimation twice. Neither one is particularly convenient. It seems as though useSpring is misbehaving when it is "initialized" with empty animation parameters.
This is what "from" is for. You're creating a spring with zero input and get an object collection back that's now empty, you distribute that among your views. Calling "set" afterwards doesn't cause a new render pass, so your view remains with empty styles.
Basically, always think of spring as a value converter. You give it something, it converts that into something else. If you use "set" to add to it, you need to inform the view. Even if it's a forceUpdate, the view has to get to the converted properties.
Ok, that makes sense to me, but in this case I don't think the spring is acting as a value converter. I can see that a non-empty object is being fed in, and I'm getting an empty object out. This all happens before a render pass is relevant. In fact, I could just do nothing with the AnimatedStyle returned by the spring and I would see the same behavior. i.e. return null here, and I still see the expected {
opacity: 1,
from: {opacity: 0},
reset: true,
} parameter going into useSpring, and {} being returned. So this isn't about informing a view, it's about useSpring not returning the expected result.
could you put up a sandbox for this? if you pass from: { opacity: 0 } in i expect it to deterministically output { opacity: animatedValue }, not { }. there has to be a bug or a misunderstanding.
Yup, here it is. https://codesandbox.io/s/75kw4671
@CaptainStiggz just passing const [animation, setAnimation] = useState({from: {opacity: 1}}) should fire the first animation as well.
so to give a little more detail on how this works internally, so when you create properties in the animation object for the first time i.e
// First render pass
const style = useSpring({x: 0, opacity:0});
each subsequent update of these values i.e
// second render pass
const style = useSpring({x: 1, opacity:1})
simply updates the value at that object reference pointer, this is how we avoid rerendering the component on every frame, as internally, Animated watches the value changes at the ref pointers and updates the dom accordingly (very simple explanation of how i understand it).
now whats happening is on initialPass, you are setting animated object, internally useSpring initializes all the values e.t.c within that renderPass. but on every subsequent renderPass, useSpring
does not update its values until useEffect is called, so any new values being introduced in these renderPasses wont be made available until the next renderPass, i.e if on the next render pass you have
// 3rd render pass
const style = useSpring({opacity: 1, x: 1, y: 1});
y wont be in the style object until the next render pass, so you need a way to inform the view that a new value as been added i.e forceUpdate.
TLDR updating already existing values in the animated object will work as only the value itself is updated, the pointer to the value remains the same, adding a new value to the animated object wont be available to the view until the next renderPass.
I hope this clarifies how it works a bit
Got it! Thanks for the detailed explanation. I'll probably end up using forceUpdate, as I want the component to perform generic animations, i.e. be able to animate any property, not just opacity.
@CaptainStiggz you can do this:
const AnimatedComponent = animated(SomeComponent)
const props = useSpring(...)
<AnimatedComponent {...props} />
@CaptainStiggz can this be closed now?
i guess so, let me know if something new comes up ...
What is should do to animate with useSpring when I don't know the initial state for animation?
For example, if I should read it from DOM: top and left offsets. There are no way to set from property to useSpring
@dchebakov You can set top and left to garbage values (like 0) until you have all the necessary data. If that results in a bad UX, you can hide the element until its actual top and left are computed. In the future, you'll be able to set initial values after the first render, and useSpring will forcefully re-render to ensure the new animated values are "hooked up" to their animated component(s).
@aleclarson, elements initially positioned with the flexbox layout, then I want drag it with the absolute position coordinates. How I can do that? I can calculate offsets only after first render happend.
@dchebakov That deserves its own issue, with a minimal CodeSandbox too please. 馃憤
Most helpful comment
@CaptainStiggz just passing
const [animation, setAnimation] = useState({from: {opacity: 1}})should fire the first animation as well.so to give a little more detail on how this works internally, so when you create properties in the animation object for the first time i.e
// First render passconst style = useSpring({x: 0, opacity:0});each subsequent update of these values i.e
// second render passconst style = useSpring({x: 1, opacity:1})simply updates the value at that object reference pointer, this is how we avoid rerendering the component on every frame, as internally, Animated watches the value changes at the ref pointers and updates the dom accordingly (very simple explanation of how i understand it).
now whats happening is on initialPass, you are setting animated object, internally
useSpringinitializes all the values e.t.c within that renderPass. but on every subsequent renderPass,useSpringdoes not update its values until
useEffectis called, so any new values being introduced in these renderPasses wont be made available until the next renderPass, i.e if on the next render pass you have// 3rd render passconst style = useSpring({opacity: 1, x: 1, y: 1});ywont be in the style object until the next render pass, so you need a way to inform the view that a new value as been added i.eforceUpdate.TLDR updating already existing values in the animated object will work as only the value itself is updated, the pointer to the value remains the same, adding a new value to the animated object wont be available to the view until the next renderPass.
I hope this clarifies how it works a bit