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

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.

Related

Intersection Observer API with react

I am trying to create a navigation tracker on a page where a user scrolls with the intersection observer API and a tracker shows what section on the page in react like something shown in the website with this link https://olaolu.dev/ with the boxes that changes on scroll (scroll indicators), I already created a code sandbox. anyone who can assist me would mean a lot. been getting errors here and there don't even know what to do again, Link to code sandbox below
https://codesandbox.io/s/tender-oskar-rvbz5?file=/src/App.js
I only used react-intersection-observer with framer motion before, I added a ref to my h1 so the observer knows if it's inView or not.
controls.start will trigger if inView is true.
import { useInView } from "react-intersection-observer";
const scrollVariant = {
hidden: {
opacity: 0,
x: 0,
transition: {
duration: 1,
},
},
visible: {
opacity: 1,
x: 250,
transition: {
duration: 1,
},
},
};
export default function Home() {
const controls = useAnimation();
const { ref, inView } = useInView({
triggerOnce: false,
});
React.useEffect(() => {
if (inView) {
controls.start("visible");
}
if (!inView) {
controls.start("hidden");
}
}, [controls, inView]);
return (
<>
<motion.h1
variants={scrollVariant}
initial="hidden"
animate={controls}
ref={ref}
>
</>
);
}

Have TimelineLite only play once on Gatsby site

I have a TimelineLite timeline set up on my Gatsby site to animate my hero section when a user navigates to a page. However, if a user clicks a link to the current page i.e. if a user is on the homepage and clicks a link to the homepage, it is reloading the page and triggering the timeline to run again. Is there a way to make sure that my current link will be inactive within Gatsby?
Hero.tsx
import React, { useEffect, useRef, useState } from 'react';
import css from 'classnames';
import { ArrowButton } from 'components/arrow-button/ArrowButton';
import { HeadingReveal } from 'components/heading-reveal/HeadingReveal';
import { gsap, Power2, TimelineLite } from 'gsap';
import { RichText } from 'prismic-reactjs';
import htmlSerializer from 'utils/htmlSerializer';
import { linkResolver } from 'utils/linkResolver';
import s from './Hero.scss';
gsap.registerPlugin(TimelineLite, Power2);
export const Hero = ({ slice }: any) => {
const linkType = slice.primary.link._linkType;
const buttonLink =
linkType === 'Link.document' ? slice.primary.link._meta : slice.primary.link.url;
const theme = slice.primary.theme;
const image = slice.primary.image;
const contentRef = useRef(null);
const headingRef = useRef(null);
const copyRef = useRef(null);
const buttonRef = useRef(null);
const [tl] = useState(new TimelineLite({ delay: 0.5 }));
useEffect(() => {
tl.to(contentRef.current, { css: { visibility: 'visible' }, duration: 0 })
.from(headingRef.current, { y: 65, ease: Power2.easeOut, duration: 1 })
.from(copyRef.current, { opacity: 0, y: 20, ease: Power2.easeOut, duration: 1 }, 0.5)
.from(buttonRef.current, { opacity: 0, y: 10, ease: Power2.easeOut, duration: 1 }, 1);
}, [tl]);
return (
<div
className={css(s.hero, s[theme])}
style={{
background: image ? `url(${image.url})` : 'white',
}}
>
<div className={s.hero__container}>
<div className={s.content__left} ref={contentRef}>
<HeadingReveal tag="h1" headingRef={headingRef}>
{RichText.asText(slice.primary.heading)}
</HeadingReveal>
<div className={s.content__copy} ref={copyRef}>
{RichText.render(slice.primary.copy, linkResolver, htmlSerializer)}
</div>
<div className={s.content__button} ref={buttonRef}>
<ArrowButton href={buttonLink}>{slice.primary.button_label}</ArrowButton>
</div>
</div>
</div>
</div>
);
};
If you are using the built-in <Link> component to make that navigation that shouldn't happen since the #reach/router doesn't trigger and doesn't re-renders when navigating on the same page. There isn't any rehydration there.
If you are using an anchor (<a>) you are refreshing the full page so all your components will be triggered again.
In addition, another workaround that may do the trick in your case is to use a useEffect with empty deps ([]), creating a componentDidMount effect. In that case, since the page is not reloaded, it won't be triggered again:
const [tl] = useState(new TimelineLite({ delay: 0.5 }));
useEffect(() => {
tl.to(contentRef.current, { css: { visibility: 'visible' }, duration: 0 })
.from(headingRef.current, { y: 65, ease: Power2.easeOut, duration: 1 })
.from(copyRef.current, { opacity: 0, y: 20, ease: Power2.easeOut, duration: 1 }, 0.5)
.from(buttonRef.current, { opacity: 0, y: 10, ease: Power2.easeOut, duration: 1 }, 1);
return () => unmountFunction() // unmount to avoid infinite triggers
}, []);
Note: you may need to make another few adjustments to make the code work since I don't know your requirements. The idea is to remove the dependency of the t1 to bypass the issue.
To avoid infinite triggers you may also need to unmount the component with return () => unmountFunction().

useSpring won't animate on prop change

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

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