Page transitions in React Spring and Next JS - reactjs

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

Related

I use UseTransition of react-spring. The whole list animates when I need only one item to be animated. What`s problem?

my problem is ItemList animation, every time when i change an item - delete for example react renders and animates the whole itemList which is unnexpected behavior
Also i`d like my items to be animated when only delete and create items, im using react-spring library
But there is also an interesting thing. If i delete items from the lowest to up gradually it works as expected but if i delete elements from top to bottom the list of items rerenders and animates fully and i don`t unredstand why.
HomePage:
import PostForm from '../components/PostForm/PostForm';
import {MemoizedToDoList} from '../components/ToDoList/ToDoList';
import { useGetToDosQuery } from '../store/rtcApi';
const HomePage = () => {
const data = useGetToDosQuery();
return (
<div>
<div className="ToDoMain">
<PostForm/>
<MemoizedToDoList items={data.data? data.data.toDos.toDos:[]} isLoading={data.isLoading}/>
</div>
</div>
)
}
export default HomePage;
ToDoList:
import React from 'react'
import { useGetToDosQuery } from '../../store/rtcApi';
import { useSelector } from 'react-redux';
import { useTransition } from 'react-spring';
import LoadingSpinner from "../LoadingSpinner/LoadingSpinner";
import ToDoItem from '../ToDoItem/ToDoItem'
import ToDoListCss from "./ToDoList.module.css";
const ToDoList = ({items, isLoading}) => {
const {toDos} = useSelector(state => state.main);
const {isAuth} = useSelector(state => state.auth);
let toDosData = [];
if(isAuth && items){
toDosData = items;
}else{
toDosData = toDos;
}
const transition = useTransition(toDosData, {
from: {x: -100, y:800, opacity: 0},
enter: {x: 0, y:0, opacity: 1},
leave: {x: 100, y: 800, opacity: 0},
keys: item => item.id,
trail: 300
});
if(isLoading)
return <LoadingSpinner scaleSet={0.5}/>;
return (
<div className={ToDoListCss.toDoList}>
{transition((style, item, key)=><ToDoItem style={style} item={item} key={key}/>)}
</div>
)
}
export const MemoizedToDoList = React.memo(ToDoList);
ToDoItem:
import React from 'react'
import { useDispatch, useSelector } from 'react-redux';
import { useRemoveToDoMutation } from "../../store/rtcApi";
import { removeToDo } from "../../store/slices/mainSlice";
import {useSpring, animated} from "react-spring";
import { BsPatchExclamationFill } from 'react-icons/bs';
import { RiDeleteBin2Line } from "react-icons/ri";
import ToDoItemCss from "./toDoItem.module.css";
const ToDoItem = ({item, style}) => {
const dispatch = useDispatch();
const {isAuth} = useSelector((state)=>state.auth);
const [ removeUserToDo ] = useRemoveToDoMutation();
const crossLineStyle = useSpring({
to: { opacity: 1, width: "65%", transform:"rotate(8deg)" },
from: { opacity: 0, width: "0%", transform:"rotate(-20deg)" },
reverse: !item.isComplete,
config: { frequency: 0.1 }
});
const onRemoveItem = React.useCallback((item) => {
if(isAuth){
return removeUserToDo(item._id);
}
dispatch(removeToDo(item.id));
}, [dispatch])
return (
<animated.div style={style} className={ToDoItemCss.toDoItem}>
<animated.div style={crossLineStyle} className={ToDoItemCss.overCrossLineAnimated}></animated.div>
<div className={ToDoItemCss.toDoItemText}>{item.title}</div>
<div className={ToDoItemCss.todoItemIconGroup}>
{item.isImportant && <div className={ToDoItemCss.todoItemImportantIcon}><BsPatchExclamationFill/></div>}
<div onClick={()=>{onRemoveItem(item)}} className='todo-item_bin-icon'><RiDeleteBin2Line/></div>
</div>
</animated.div>
)
}
export default ToDoItem;
I was trying to use memo and useCallBack but i think i don`t get how shoud i properly use it here with the RTC query and redux state.
Chunks of code from ToDoList:
const transition = useTransition(toDosData, {
from: {x: -100, y:800, opacity: 0},
enter: {x: 0, y:0, opacity: 1},
leave: {x: 100, y: 800, opacity: 0},
keys: item => item.id,
trail: 300
});
if(isLoading)
return <LoadingSpinner scaleSet={0.5}/>;
return (
<div className={ToDoListCss.toDoList}>
{transition((style, item, key)=><ToDoItem style={style} item={item} key={key}/>)}
</div>
)
export const MemoizedToDoList = React.memo(ToDoList);
and here i have used useCallback and i even dono why =))
ToDoItem
const onRemoveItem = React.useCallback((item) => {
if(isAuth){
return removeUserToDo(item._id);
}
dispatch(removeToDo(item.id));
}, [dispatch])
Here how it looks like

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

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.

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().

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

Resources