How to make react-spring <Spring> replay animation? - reactjs

I have a simple react site which displays drinks from the drinks-api. It basically works like this: when user presses button, the query gets updated, fetchItems runs and items are displayed this way
<Spring from={{ opacity: 0, marginTop: 100 }} to={{ opacity: 1, marginTop: 0 }} config={{ delay: 400 }}>
{(props) => (
<div style={props}>
{drinks
? drinks.map((drink) => (
<DrinkCard
title={drink.strDrink}
image={drink.strDrinkThumb}
alcohol={drink.strAlcoholic}
category={drink.strCategory}
/>
))
: () => (
<div className="col-6 col-md-4 p-0">
<h1>NO RESULTS</h1>
</div>
)}
</div>
)}
</Spring>
For now this animation works only once when page loads. How do I force it to run on every search?

You should add reset to your Spring component like this:
<Spring
from={{ opacity: 0, marginTop: 100 }}
to={{ opacity: 1, marginTop: 0 }}
config={{ delay: 400 }}
reset
>
Here is an example: https://codesandbox.io/s/dank-moon-sq7v9?file=/src/App.js:348-503

Related

Animating height using Framer Motion not working in React

So, I have been trying to use Framer Motion for my React project. I basically want to animate the height from 0 to "auto", when the div gets rendered.
I tried the below code, but the height is not getting animated
<motion.div
initial={{ height: 0 }}
animate={{ height: "auto" }}
transition={{ duration: 0.5 }}
key={searchQuery?.length}
>
When I replaced height with width, the animation works fine, but can't figure out why the height is not getting animated. And I was unable to find any proper documentation regarding this.
Here is the CodeSandbox Link for demo.
Fixed versinn
What was wrong?
Your conditional rendering logic was in the wrong place, AnimatePresence works only if its direct child disappears.
exit prop was missing
key prop has to be stable, it can't be the length of the string
overflow: hidden have to be added so the children are invisible
Final code:
export default function App() {
const ref = useRef(null);
const [isActive, setIsActive] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<div>
<input
placeholder={"Enter Keyword"}
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
}}
/>
<AnimatePresence>
{searchQuery?.length >= 1 && (
<motion.div
style={{ overflow: "hidden" }}
initial={{ height: 0 }}
animate={{ height: "auto" }}
transition={{ duration: 0.5 }}
exit={{ height: 0 }}
key={"container"}
>
{dataList?.map((listItem) => (
<div
style={{
padding: "1rem",
color: "#E090EE",
borderBottom: "1px solid #E1E1E1",
}}
>
{listItem.activity_title}
</div>
))}
</motion.div>
)}
</AnimatePresence>
</div>
</div>
);
}

When new items are added, the first existing items are not animated using framer motion

When there are only two items initially, clicking Add will add new item (3rd item). The 2nd item will go below and show a heading. Going below is not animated.
In the same way, when there are 3 items and clicked Delete, the last movement is not animated.
Demo with full source can be found here: https://codesandbox.io/s/practical-dream-229d2y?file=/src/App.tsx
This is the source code for archive purposes:
import { useState } from "react";
import { AnimatePresence, AnimateSharedLayout, motion } from "framer-motion";
function SomeItem() {
return (
<div
style={{
border: "1px solid blue",
padding: "12px",
height: 60,
width: "100%",
margin: "12px"
}}
>
{new Date().toISOString()}
</div>
);
}
export default function App() {
const [items, setItems] = useState<typeof SomeItem[]>([SomeItem, SomeItem]);
return (
<div className="App">
<button
onClick={() =>
setItems((prev) => prev.filter((_p, i) => i !== prev.length - 1))
}
>
Delete
</button>
<button onClick={() => setItems((prev) => [...prev, SomeItem])}>
Add
</button>
<section>
<h3>What&apos;s not working?</h3>
<p>
When there are only two items initially, clicking Add will add new
item (3rd item). The 2nd item will go below and show a heading. Going
below is not animated.
</p>
<p>
In the same way, when there are 3 items and clicked Delete, the last
movement is not animated.
</p>
</section>
<AnimateSharedLayout>
<ul style={{ listStyle: "none", margin: 0, padding: 0 }}>
<AnimatePresence initial>
{items.map((Item, i) => (
<motion.li
key={i}
layout
animate={{ transition: { damping: 1000 }, y: 0 }}
exit={{ opacity: 0, transition: { damping: 1000 }, y: "-110%" }}
initial={{ y: "-110%" }}
>
{i !== 0 && items.length > 2 && (
<motion.h3
animate={{ transition: { damping: 0 }, x: 0 }}
exit={{ x: "-100%" }}
initial={{ x: "-100%" }}
>
Item {i}
</motion.h3>
)}
<Item />
</motion.li>
))}
</AnimatePresence>
</ul>
</AnimateSharedLayout>
</div>
);
}
The problem seems to be at this conditional i !== 0 && items.length > 2, when the items.length is greater than two it's rendering two headers at same time because you specified to only render the header after the item of index 0, thus when you delete the item it's also deleting the two headers at same time and just animating the last one. To fix it you can change the conditional to i !== 0 && items.length > 1, this way it will render item 1, item 2 and so on. see the working example

AnimatePresence show both elements when animating

I am to make a step-like animation, hence, when I click a button, another div than the previous is shown, e.g:
import { motion, AnimatePresence } from 'framer-motion'
const MyApp= props => {
const [count, setCount] = useState(0)
return (
<>
<AnimatePresence>
{count == 0 && (
<motion.div
transition={{ duration: 2 }}
initial={{ opacity: 1 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
Hello 1
<button onClick={() => { setCount(count + 1) }}
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{count == 1 && (
<motion.div
transition={{ duration: 2 }}
initial={{ opacity: 1 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
Hello 2
<button onClick={() => { setCount(count + 1) }}
</motion.div>
)}
</AnimatePresence>
</>
)
}
export default MyApp
However, the issue is, that when one div is fading out/in, the other is also fading in/out, hence, I have two elements at the same time. Instead I thought it would just animate one, remove it, and animate the other.
Am I doing something wrong here ?
Since you want to animate them together they should be under one <AnimatePresence />
AnimatePresence requires you to add an explicit key on the component you are animating beneath it so that it can track the components as they are added and removed https://www.framer.com/api/motion/animate-presence/#unmount-animations
Since you want to replace one component with another you need to add the prop exitBeforeEnter on the AnimatePrensence. https://www.framer.com/api/motion/animate-presence/#animatepresenceprops.exitbeforeenter
<AnimatePresence exitBeforeEnter> // Note on #3
{isGreenBox ? (// Note on #1
<GreenBox
initial={{ scale: 0 }}
animate={{ scale: 1 }}
exit={{ scale: 0 }}
key="green" // Note on #2
/>
) : (
<PurpleBox
initial={{ scale: 0 }}
animate={{ scale: 1 }}
exit={{ scale: 0 }}
key="purple" // Note on #2
/>
)}
</AnimatePresence>
Here is a working example I created :) https://codesandbox.io/s/framer-motion-animation-presence-exitbeforeenter-0miq0

React + Spring - feels laggy

My page which downloads and displays data from API does Spring animations and they are really laggy. It is quite ok when there are like 1-5 items (but still choppy) and when there are like 20-30 items it's just horrible.
This is my component which animates:
const FadeInSpring = ({ children, del }) => {
return (
<Spring from={{ opacity: 0, marginTop: 100 }} to={{ opacity: 1, marginTop: 0 }} config={{ delay: del }}>
{(props) => <animated.div style={props}>{children}</animated.div>}
</Spring>
);
};
And this is card I animate:
<section className="row justify-content-center p-1">
<article className="col-12 col-md-5">
<div className="card shadow">
<div className="card-body">
<h2 className="card-title">Ingredients:</h2>
<p>{item.strIngredient1}</p>
</div>
</div>
</article>
</section>;
Everything together just looks like this:
<FadeInSpring>
<HeaderCard/>
</FadeInSpring>
Am I doing something wrong?
EDIT: Adding the link so you can see it yourself. Maybe you will notice the problem. https://ablaszkiewicz.github.io/react-portfolio/#/recipes

Animation duration in React Spring

I have a fairly simple fade-in scenario, where I want to control the duration of the animation. But cannot wrap around my head around how to accomplish this.
Code excerpt:
function BoxA() {
return (
<Spring from={{ opacity: 0.2 }} to={{ opacity: 1 }}>
{props => (
<div
style={{
height: 100,
width: 100,
backgroundColor: "pink",
color: "#fff",
...props
}}
>
1
</div>
)}
</Spring>
);
}
Complete code example: https://codesandbox.io/s/n7pw660264
You have to set the config property for the duration.
<Spring config={{duration: 5000}} from={{ opacity: 0.2 }} to={{ opacity: 1 }}>
You can use delay property to control the animation,
According to documentation
Delay in ms before the animation starts (config.delay takes precedence
if present) */
like this
<Spring from={{ opacity: 0.2 }} delay={1000} to={{ opacity: 1 }}></Spring>
Demo
Source

Resources