useSpring won't animate on prop change - reactjs

As opposed to what is mentioned in Spring's documentation, useSpring does not animate my counter component on prop change:
If you re-render the component with changed props, the animation will update.
I've tried passing the props as children, to no effect. What am I missing? Here's a demo:
https://codesandbox.io/s/spring-counter-jsylq?file=/src/App.js:199-230
import React, { useState } from "react";
import { animated, useSpring } from "react-spring";
const Counter = ({ value }) => {
const anim = useSpring({ from: { opacity: 0 }, to: { opacity: 1 } });
return <animated.h1 style={anim}>{value}</animated.h1>;
};
export default function App() {
const [count, setCount] = useState(0);
return (
<>
<Counter value={count} />
<button onClick={() => setCount(count + 1)}>increment</button>
</>
);
}

It only said the animation will update. But now the animation is kind of static.
You can do a couple of things. If you add the reset property then it will repeat the initial animation at every re-render.
const anim = useSpring({ from: { opacity: 0 }, to: { opacity: 1 } , reset: true});
Or you can add some properties depending of the count value. For example the parity. Odd became blue, even number to red.
const anim = useSpring({ from: { opacity: 0 }, to: { opacity: 1 , color: value % 2 === 0 ? 'red' : 'blue'} });
What do you think?
https://codesandbox.io/s/spring-counter-forked-znnqk

Related

Slideshow effect with Framer Motion

When some prop in my component changes in framer motion, I would like to fade that property out, then fade the "new" property value in?
Here's the timeline of the animation as I imagine it:
0: Prop Value Changes, old value start to fade out
.5: New value visible
1: New value finishes fading in
But the only way I see to do this with framer is to use Timeouts. Is there some way other than using timeouts to achieve this effect?
Codesandbox
import { motion } from "framer-motion";
import { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [seconds, setSeconds] = useState(0);
const [anim, setAnim] = useState("in");
useEffect(() => {
const interval = setInterval(() => {
setSeconds((seconds) => seconds + 1);
setAnim("in");
setTimeout(() => {
setAnim("out");
}, 500);
}, 1000);
return () => clearInterval(interval);
}, []);
const variants = {
out: {
opacity: 0
},
in: {
opacity: 1,
transition: {
duration: 0.5
}
}
};
return (
<motion.div
animate={anim}
variants={variants}
className="App"
style={{ fontSize: 100 }}
>
{seconds}
</motion.div>
);
}
You can do this with AnimatePresence.
Wrap your motion.div with an AnimatePresence tag, and use the seconds as a unique key for your div. The changing key will trigger AnimatePresence to animate the div in and out each time it changes (because new key means it's a different element).
To get this to work, you'll need to define your animations on the initial, animate, and exit props.
You'll also want to be sure to set the exitBeforeEnter prop on AnimatePresence so the fade out animation completes before the fade in starts.
Sandbox Example
export default function App() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds((seconds) => seconds + 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return (
<AnimatePresence exitBeforeEnter>
<motion.div
initial={{opacity:0}}
animate = {{opacity: 1, transition:{duration: 0.5}}}
exit={{opacity: 0 }}
className="App"
style={{ fontSize: 100 }}
key={seconds}
>
{seconds}
</motion.div>
</AnimatePresence>
);
}

Framer Motion dynamic variants don't work when modifying initial properties

According to the docs I can make variant properties dynamic: https://www.framer.com/docs/animation/##dynamic-variants.
But this doesn't work when I try to make the initial properties dynamic.
For example:
import React, { useState, useEffect } from "react";
import { motion, useAnimation } from "framer-motion";
//make div appear from either bottom or right, depending on "origin" custom prop
const variant = {
hidden: (origin) =>
origin === "bottom"
? { x: 0, y: 200, opacity: 0 }
: { x: 200, y: 0, opacity: 0 },
visible: { x: 0, y: 0, opacity: 1, transition: { duration: 1 } },
};
function App() {
const [origin, setOrigin] = useState("bottom");
const controls = useAnimation();
//after 2 secs make origin "right"
useEffect(() => {
setTimeout(() => {
setOrigin("right");
}, 2000);
}, []);
//after 4 secs start the animation
useEffect(() => {
setTimeout(() => {
controls.start("visible");
}, 4000);
}, [controls]);
return (
<motion.div
style={{ width: 100, height: 50, background: "red" }}
variants={variant}
initial="hidden"
animate={controls}
custom={origin}
/>
);
}
export default App;
Here I made a dynamic variant to make a div appear from either the right or bottom, which I can control from a custom prop. Initially this custom prop is set to "bottom". After 2 secs, this is changed to "right". When I start the animation after 4 secs, I expect the div to appear from the right but it still appears from the bottom:
This is because the component is already rendered and is still the same component even if the origin prop being passed to the component has changed.
You can do two things:
Use a isVisible state variable where the render method will observe for changes and render the component when it becomes true.
function App() {
const [isVisible, setIsVisible] = useState(false);
...
//after 4 secs start the animation
useEffect(() => {
setTimeout(() => {
setIsVisible(true);
controls.start("visible");
}, 4000);
}, [controls]);
return (
isVisible && (
<motion.div
...
/>
)
);
}
DEMO
Add a key prop to the component with the origin value so that when the value changes, React will re-render the component.
function App() {
...
return (
<motion.div
key={origin}
...
/>
);
}
DEMO
2nd option may be your preferred choice if you need to toggle between the origin.

How to change opacity with clickEvent in React?

I'm trying to change opacity of the span below so that I can show the user that the text is copied.
before copyToClipboard clicked => opacity: 0
after clicked => opacity: 1 for about 1 sec and again to opacity:0
I know onCopy={timer} wouldn't work but I really can't figure out how to approach.
import React, { useState, useEffect } from "react"
import { CopyToClipboard } from "react-copy-to-clipboard"
const Contact = () => {
const [style, setStyle] = useState({ opacity: 0 })
useEffect(() => {
const timer = setTimeout(function () {
setStyle({ opacity: 1 })
}, 1000)
}, [])
return (
<div>
<CopyToClipboard text='something' onCopy={timer}>
<LogoImage />
</CopyToClipboard>
<span style={style}>
copied!
</span>
</div>
I think you don't need useEffect for this case. Just create timer function outside useEffect like below:-
import React, { useState, useEffect } from "react"
import { CopyToClipboard } from "react-copy-to-clipboard"
const Contact = () => {
const [style, setStyle] = useState({ opacity: 0 })
const timer = () => {
setStyle({ opacity: 1 });
setTimeout(() => {
setStyle({ opacity: 0 });
}, 1000);
};
return (
<div>
<CopyToClipboard text='something' onCopy={() => timer()}>
<LogoImage />
</CopyToClipboard>
<span style={style}>
copied!
</span>
</div>
)
Instead of using setTimeout,you can use simple CSS animations to achieve the following -
Create a CSS animation using keyframes
Create a state to cause the animation and change state onCopy.
And that's it.
Check the code here, for ref-
https://codesandbox.io/s/festive-northcutt-kb32u?file=/src/App.js
I personally would not use timers for UI updates - ever. It does work (in this case), but it is not clean. I would suggest using CSS and a transition or animation. Removing/clearing the class name in onAnimationEnd makes sure the animation will trigger every time it is clicked. The animation duration is set to 1,4s out of which (0,2/1,4) = 14,29% are for fading in and 14,29% for fading out, that leaves 1s for the span to be shown
CSS:
.copied {
opacity: 0;
display:none;
}
.flash {
display:inline-block;
animation: flash 1.4s ease-in-out both;
}
#keyframes flash {
0: {
opacity: 0;
}
14.29% {
opacity: 1;
}
85.71% {
opacity: 1;
}
100% {
opacity: 0
}
}
(Simplified your example to remove the dependency on clipboard):
const Contact = () => {
const [flash, setFlash] = React.useState("")
const onClick = (event) => {
setFlash("flash");
}
const onAnimationEnd = (event) => {
setFlash("");
}
return (
<div onClick={onClick}>
Something
<span className={`copied ${flash}`} onAnimationEnd={onAnimationEnd}>
copied!
</span>
</div>
)
}
Like in your example this has the downside that the element has to exist all the time (even though now with display:none removed from rendering).
An even better approach would be to use the amazing TransitionGroup/CSSTransition from react-transition-group to add/remove the element. Admittedly, a bit much for this example, but in general the better and cleaner way to go.

useTransition mounts new object instantly

I am trying to figure out how to utilise useTransition for page transitions (simple opacity change where first page fades out and new one fades in).
So far I have this small demo going https://codesandbox.io/s/sleepy-knuth-xe8e0?file=/src/App.js
it somewhat works, but weirdly. When transition starts new page is mounted instantly while old one starts animating. This causes various layout issues and is not behaviour I am after. Is it possible to have first element fade out and only then mount and fade in second element?
Code associated to demo
import React, { useState } from "react";
import "./styles.css";
import { useTransition, a } from "react-spring";
export default function App() {
const [initial, setInitial] = useState(true);
const transition = useTransition(initial, {
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 }
});
return (
<div>
{transition((style, initial) => {
return initial ? (
<a.h1 style={style}>Hello Initial</a.h1>
) : (
<a.h1 style={style}>Hello Secondary</a.h1>
);
})}
<button onClick={() => setInitial(prev => !prev)}>Change Page</button>
</div>
);
}
you can delay the start of the transition by waiting for the leave animation to complete.
const sleep = t => new Promise(res => setTimeout(res, t));
...
const transition = useTransition(initial, {
from: { position: "absolute", opacity: 0 },
enter: i => async next => {
await sleep(1000);
await next({ opacity: 1 });
},
leave: { opacity: 0 }
});
This delays the animation also for the very first time it is run. You can have a ref to keep track of whether the component has been rendered before or if it is its first time rendering, then you can skip sleep call if it's the first render.
OR
You can just simply provide trail config
const transition = useTransition(initial, {
from: { position: "absolute", opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
trail: 300
});
You need to add position: absolute and then you need to set the right position with css.
import React, { useState } from "react";
import "./styles.css";
import { useTransition, a } from "react-spring";
export default function App() {
const [initial, setInitial] = useState(true);
const transition = useTransition(initial, {
from: { position: 'absolute', opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 }
});
return (
<div>
{transition((style, initial) => {
return initial ? (
<a.h1 style={style}>Hello Initial</a.h1>
) : (
<a.h1 style={style}>Hello Secondary</a.h1>
);
})}
<button onClick={() => setInitial(prev => !prev)}>Change Page</button>
</div>
);
}

useTransition with react-spring as a component changes

I'm attempting to animate a card in and out. If there is a selected value, the card appears. If the selected item is undefined, the card disappears. I got this to work.
The next thing I tried to do is make it that if the selection changed (A new item) - animate out a card and animate in a new one. I'm confused on how to make this work... here is what I've attempted that kind of works.
Clearly I'm not understanding how this should be done. I'm wondering if I need to break this up into two cards and run useChain.
const App: React.FC = () => {
//...
const [selectedItem, setSelectedItem] = useState<TimelineItem | undefined>(undefined);
const [lastSelectedItem, setLastSelectedItem] = useState<TimelineItem>({
content: '',
start: new Date(),
id: 0,
});
//...
const transitions = useTransition(
[selectedItem, lastSelectedItem],
item => (item ? item.id : 0),
{
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 },
}
);
return (
<Timeline
onItemSelect={item => {
if (selectedItem) setLastSelectedItem(selectedItem);
setSelectedItem(item);
}}
/>
{transitions.map(({ item, key, props }) => {
return (
item && (
<animated.div style={props}>
{item === selectedItem ? (
<ItemDetails
item={selectedItem} // If the selected item is undefined, this will not be running (happens when unselecting something)
groups={groups}
key={key || undefined} // key becomes undefined since item is
></ItemDetails>
) : (
false && ( // The last item never shows, it still has the data for the lastSelectedItem (For the fade out while the new Item is being shown or there is no new item).
<ItemDetails
item={lastSelectedItem}
groups={groups}
key={key || undefined}
></ItemDetails>
)
)}
</animated.div>
)
);
})}
);
};
If I understand you well, you want to display the state of an array. New elements fade in and old one fades out. This is the functionality the Transition created for. I think it can be done a lot simpler. I would change the state managment and handle the array in the state. And the render should be a lot simpler.
UPDATE:
I created an example when the animation of the entering element wait for the animation of the leaving element to finish.
I made it with interpolation. The o value changes from 0 to 1 for enter, and 1 to 2 for leave. So the opacity will change:
leave: 1 -> 0 -> 0
enter: 0 -> 0 -> 1
Here is the code:
import React, { useState, useEffect } from "react";
import { useTransition, animated } from "react-spring";
import ReactDOM from "react-dom";
import "./styles.css";
function App() {
const [cards, set] = useState(["A"]);
useEffect(() => {
setInterval(() => {
set(cards => (cards[0] === "A" ? "B" : "A"));
}, 4000);
}, []);
const transitions = useTransition(cards, null, {
from: { o: 0 },
enter: { o: 1 },
leave: { o: 2 },
config: { duration: 2000 }
});
return transitions.map(({ item, key, props }) => (
<div style={{ fontSize: "300px" }}>
<animated.div
style={{
position: "absolute",
opacity: props.o.interpolate([0, 0.5, 1, 1.5, 2], [0, 0, 1, 0, 0])
}}
>
{item}
</animated.div>
</div>
));
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
working example: https://codesandbox.io/s/react-spring-staggered-transition-xs9wy

Resources