useTransition with react-spring as a component changes - reactjs

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

Related

How to do a 3D carousel wth React

I cannot find how to do a 3D carousel (aka slideshow) with React being able to show at least three elements.
There seems not to be up-to-date libraries or components for that in npm :(
Here is what it should look like:
After experimenting for a while, here is how I manage to do it using framer motion:
import './styles.css';
import { AnimatePresence, motion } from 'framer-motion';
import { useState } from 'react';
export default function App() {
const [[activeIndex, direction], setActiveIndex] = useState([0, 0]);
const items = ['🍔', '🍕', '🌭', '🍗'];
// we want the scope to be always to be in the scope of the array so that the carousel is endless
const indexInArrayScope =
((activeIndex % items.length) + items.length) % items.length;
// so that the carousel is endless, we need to repeat the items twice
// then, we slice the the array so that we only have 3 items visible at the same time
const visibleItems = [...items, ...items].slice(
indexInArrayScope,
indexInArrayScope + 3
);
const handleClick = newDirection => {
setActiveIndex(prevIndex => [prevIndex[0] + newDirection, newDirection]);
};
return (
<div className="main-wrapper">
<div className="wrapper">
{/*AnimatePresence is necessary to show the items after they are deleted because only max. 3 are shown*/}
<AnimatePresence mode="popLayout" initial={false}>
{visibleItems.map((item) => {
// The layout prop makes the elements change its position as soon as a new one is added
// The key tells framer-motion that the elements changed its position
return (
<motion.div
className="card"
key={item}
layout
custom={{
direction,
position: () => {
if (item === visibleItems[0]) {
return 'left';
} else if (item === visibleItems[1]) {
return 'center';
} else {
return 'right';
}
},
}}
variants={variants}
initial="enter"
animate="center"
exit="exit"
transition={{ duration: 1 }}
>
{item}
</motion.div>
);
})}
</AnimatePresence>
</div>
<div className="buttons">
<motion.button
whileTap={{ scale: 0.8 }}
onClick={() => handleClick(-1)}
>
◀︎
</motion.button>
<motion.button whileTap={{ scale: 0.8 }} onClick={() => handleClick(1)}>
▶︎
</motion.button>
</div>
</div>
);
}
const variants = {
enter: ({ direction }) => {
return { scale: 0.2, x: direction < 1 ? 50 : -50, opacity: 0 };
},
center: ({ position }) => {
return {
scale: position() === 'center' ? 1 : 0.7,
x: 0,
zIndex: zIndex[position()],
opacity: 1,
};
},
exit: ({ direction }) => {
return { scale: 0.2, x: direction < 1 ? -50 : 50, opacity: 0 };
},
};
const zIndex = {
left: 1,
center: 2,
right: 1,
};
Here is a code sandbox with the solution:
https://codesandbox.io/s/react-3d-carousel-wth-framer-motion-rtn6vx?file=/src/App.js

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.

React Spring - Animate list - one item in list at a time

Given a list:
li {
display: inline-block;
color: #000000;
}
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
<li>Item 4</li>
</ul>
Using react-spring, I am trying to animate the colour of each item in a list (one by one) every 3 seconds and loop from start to finish.
For example:
From - Color starts as black
Enter - Color changes to red
Leave - Color changes back to black
I can get an individual item to show and colour to update and then hides (as only 1 item from the list is being animated), but not the whole list to show and change the colour of each item 1 by 1.
const ColourListTransition = (items, delay) => {
const [index, setIndex ] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setIndex((state) => ( state + 1 ) % items.length);
}, delay);
return () => clearInterval(interval);
}, []);
return useTransition(items[index], {
from: { color: '#000000' },
enter: { color: "#FF0000" },
leave: { color: "#000000" },
loop: true,
config: config.molasses
})
}
{ ColourListTransition(['item 1', 'item 2', 'item 3', 'item 4'], 3000)(({ color }, item) => (
<animated.li
key={ item }
style={ { color, display: 'inline-block', listStyleType: 'none' } }
>
{ item }
</animated.li>
)) }
I played around with useTransition but I just could not figure it out. It was easy when I switched to using useSprings instead. So this might not be the most elegant solution but it works.
We create an array of spring animations which correspond to the items in your list. The color of each item is based on whether its index matches the active index from state. enter and leave don't really come into play here since you always have same 4 items in your array. I kept your useState and useEffect hooks the same.
import React, { useState, useEffect } from "react";
import { animated, config, useSprings } from "#react-spring/web";
const ColourListTransition = ({delay, items, activeColor, inactiveColor}) => {
const [index, setIndex] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setIndex((state) => (state + 1) % items.length);
}, delay);
return () => clearInterval(interval);
}, []);
const [springs] = useSprings(
// count
items.length,
// properties as a function of index
(i) => ({
// determine the current color based on the index
color: i === index ? activeColor : inactiveColor,
// all items start out black
from: { color: inactiveColor },
config: config.molasses
}),
// dependency on index state
[index]
);
return (
<ul>
{springs.map(({ color }, i) => (
<animated.li
key={items[i]}
style={{ color, display: "inline-block", listStyleType: "none" }}
>
{items[i]}
</animated.li>
))}
</ul>
);
};
export default function App() {
return (
<ColourListTransition
items={["item 1", "item 2", "item 3", "item 4"]}
delay={3000}
activeColor={"#FF0000"}
inactiveColor={"#000000"}
/>
);
}
CodeSandbox Link

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>
);
}

How to invert animation order in react-spring's useTransitions

I want to animate the opacity of the items from number 1 to 4, but want to run it inverted (from 4 to 1) if the items are removed. I thought that the reverse flag could help, but it doesn't do anything:
import React, { useState } from "react";
import { animated, config, useTransition } from "react-spring";
export default function App() {
const items = [1, 2, 3, 4];
const [isToggled, setToggled] = useState(false);
const transitions = useTransition(isToggled ? items : [], item => item, {
config: config.gentle,
unique: true,
trail: 250,
reverse: isToggled ? false : true,
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 }
});
return (
<div className="App">
<button onClick={() => setToggled(!isToggled)}>Toggle</button>
{transitions.map(({ item, key, props }) => (
<animated.div key={key} style={props}>
Issue #{item}
</animated.div>
))}
</div>
);
}
CodeSandbox
The problem with reverse method is it reverse all the content inside the array.
You only need to reverse the props properties inside the result of your useTransition.
With simple array modification like this (in typescript) :
// utils/animation.ts
// or js just modify the type
import { UseTransitionResult } from 'react-spring';
export function reverseTransition<T, Result extends UseTransitionResult<T, object>>(
arr: Result[],
): Result[] {
const result: Result[] = [];
for (let idx = 0; idx < arr.length; idx++) {
result.push({
...arr[idx],
props: arr[arr.length - 1 - idx].props,
});
}
return result;
}
and pass the result of useTransition hooks like this :
import React, { useState } from "react";
import { animated, config, useTransition } from "react-spring";
// import above code
import { reverseTransition } from "utils/animation";
export default function App() {
const items = [1, 2, 3, 4];
const [isToggled, setToggled] = useState(false);
const transitions = useTransition(isToggled ? items : [], item => item, {
config: config.gentle,
unique: true,
trail: 250,
reverse: isToggled ? false : true,
from: { opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 }
});
return (
<div className="App">
<button onClick={() => setToggled(!isToggled)}>Toggle</button>
{(isToggled ? transitions : reverseTransition(transitions)).map(({ item, key, props }) => (
<animated.div key={key} style={props}>
Issue #{item}
</animated.div>
))}
</div>
);
}
You will get the reversed animation with the same content.
I hope it helps!
Codesandbox
Notes: I am using React Spring v8, not v9 (the one that you use in your Codesandbox)
Regards

Resources