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().
Related
So I'm trying to create a simple page transition between pages in next.js using react-spring, however I can't get the unmounting component to fade out. Instead it just snaps out of the screen, while the new one fades in:
The screen then loads itself underneath, I'm really struggling to work out what's going on?
I've tried adding absolute positions to the from and/or the leave keys, but to no avail
//_app.js
import "../styles/globals.css";
import { useTransition, animated } from "react-spring";
import { useRouter } from "next/router";
function MyApp({ Component, pageProps }) {
const router = useRouter();
const transition = useTransition(router, {
key: router.pathname,
from: { opacity: 0},
enter: { opacity: 1 },
leave: { opacity: 0},
config: { duration: 1000 },
// reset:true,
});
return transition((style, item) => {
return (
<animated.div style={style}>
<Component {...pageProps} />
</animated.div>
);
});
}
export default MyApp;
Any help would be great! Thank you
I think the reason this is going wrong for you is that you're using the Component from my app to render, but passing in the useRouter state to the useTransition hook, resulting in them being disconnected.
I got it to work using an additional array:
function MyApp({ Component, pageProps }) {
const router = useRouter();
// initial state
const [compoentArray, setComponentArray] = useState([
<Component key={router.pathname} {...pageProps} />,
]);
const transitions = useTransition(compoentArray, {
from: { opacity: 0 },
enter: [{ opacity: 1 }],
leave: {
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
opacity: 0,
},
delay: 200,
});
// Updates the array when needed. Avoids rerenders
useEffect(() => {
if (compoentArray[0].key === router.pathname) {
return;
}
setComponentArray([<Component key={router.pathname} {...pageProps} />]);
}, [Component, pageProps, router, compoentArray]);
return (
<div>
{transitions((style, item) => {
// Render items managed by react-spring
return <animated.div style={style}>{item}</animated.div>;
})}
</div>
);
}
I'm trying to assign the same animation to multiple instances of a component, using Framer Motion and the react-intersection-observer package
import { useEffect, useRef, useCallback } from "react";
import { motion, useAnimation } from "framer-motion";
import { useInView } from "react-intersection-observer";
const levels = [
{
title: "GROUP LESSONS",
description:
"Lorem ipsum",
},
{
title: "WORKSHOPS",
description:
"Lorem ipsum",
},
];
const container = {
show: {
transition: {
staggerChildren: 0.2,
},
},
};
const item = {
hidden: { opacity: 0, x: 200 },
show: {
opacity: 1,
x: 0,
transition: {
ease: [0.6, 0.01, -0.05, 0.95],
duration: 1.6,
},
},
};
const Levels = () => {
const animation = useAnimation();
const [levelRef, inView] = useInView({
triggerOnce: true,
});
useEffect(() => {
if (inView) {
animation.start("show");
}
}, [animation, inView]);
return (
<LevelsContainer>
{levels.map((level, index) => {
return (
<LevelsWrapper
key={index}
ref={levelRef}
animate={animation}
initial="hidden"
variants={container}
>
<Level variants={item}>
<Title>{level.title}</Title>
<Description>{level.description}</Description>
</Level>
</LevelsWrapper>
);
})}
</LevelsContainer>
);
};
This results in the animation loading only when scrolling to the last LevelWrapper component. Then "inView" is set to true and all the components animate at the same time. In the react-intersection-observer package documentation, there's some info about wrapping multiple ref assignments in a single useCallback, so I've tried that:
const animation = useAnimation();
const ref = useRef();
const [levelRef, inView] = useInView({
triggerOnce: true,
});
const setRefs = useCallback(
(node) => {
ref.current = node;
levelRef(node);
},
[levelRef]
);
useEffect(() => {
if (inView) {
animation.start("show");
}
}, [animation, inView]);
return (
<LevelsContainer>
{levels.map((level, index) => {
return (
<LevelsWrapper
key={index}
ref={setRefs}
animate={animation}
initial="hidden"
variants={container}
>
<Level variants={item}>
<Title>{level.title}</Title>
<Description>{level.description}</Description>
</Level>
</LevelsWrapper>
);
})}
</LevelsContainer>
);
But the animations still don't trigger individually for each LevelWrapper component. What's happening?
No idea why the code in the question doesn't work but I found the final result can be reached without using neither useEffect, useRef, useCallback, , useAnimation or useInView.
In the Framer Motion documentation:
Motion extends the basic set of event listeners provided by React with a simple yet powerful set of UI gesture recognisers.
It currently has support for hover, tap, pan, viewport and drag
gesture detection. Each gesture has a series of event listeners that
you can attach to your motion component.
Then applied whats explained here: https://www.framer.com/docs/gestures/#viewport-options
<LevelsWrapper
key={index}
initial="hidden"
whileInView="show"
variants={container}
viewport={{ once: true, amount: 0.8, margin: "200px" }}
>
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}
>
</>
);
}
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.
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>
);
}