I can't figure out how to animate routing with <AnimateSharedLayout />
component by framer-motion.
Basically in the code down here i want to display a list of images, and when clicking on them i want to navigate to /images/[image_id]
and display it wide. I don't have the images locally so I have to fetch them. There are some problems here:
- The same element gets fetched one first time on the index page, then gets refetched on the detail page (and it would be nice if there was a way to bring it over to the detail page)
- Before changing route, the page automatically scrolls to the top, ruining the animation;
- If the user is on a slow network, the animation is laggy or not even kicking in;
- going back to the previous route doesn't replay the animation (I may have to cache api calls to avoid refetching them all?)
here's the code _app.tsx
import '../styles/globals.css';
import 'bulma/css/bulma.css';
import { AppProps } from "next/app";
import { AnimateSharedLayout, AnimatePresence } from 'framer-motion';
const MyApp: React.FC<AppProps> = ({ Component, pageProps, router }) => {
return (
<AnimateSharedLayout>
<AnimatePresence>
<Component {...pageProps} key={router.route} />
</AnimatePresence>
</AnimateSharedLayout>
);
}
export default MyApp;
here's pages/index.tsx
import { motion } from "framer-motion";
import { useEffect, useState } from "react";
import Picture, { PictureProps } from "./components/Picture";
export const base = 'https://jsonplaceholder.typicode.com';
const Home: React.FC = () => {
const [pics, setPics] = useState<PictureProps[]>([]);
useEffect(() => {
fetch(`${base}/photos?_start=0&_limit=15`)
.then(res => res.json())
.then(res => setPics(res as PictureProps[]))
.catch(err => console.log("fetching error", err));
}, []);
return (
<div className="container pt-4">
<div className="columns is-multiline">
{
pics.map(pic => (
<div key={pic.id} className="column is-one-third-desktop is-half-tablet is-full-mobile">
<Picture {...pic} />
</div>
))
}
</div>
</div>
);
}
export default Home;
here's pages/images/[image].tsx
import { motion } from "framer-motion";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { PictureProps } from "../components/Picture";
import { base } from '../index';
const Image: React.FC = () => {
const router = useRouter();
const { image } = router.query;
const [pic, setPic] = useState<PictureProps>({} as PictureProps);
useEffect(() => {
image && fetch(`${base}/photos/${image}`)
.then(res => res.json())
.then(res => setPic(res))
.catch(err => console.log(err));
}, [image]);
return (
<motion.div>
{
image &&
<motion.figure layoutId={`img-${image}`} className='image'>
<img src={pic.url} alt={pic.title} />
</motion.figure>
}
</motion.div>
);
}
export default Image;
and here's components/Picture.tsx
import { motion, Variants } from "framer-motion";
import Link from "next/link";
export type PictureProps = {
albumId: number,
id: number,
title: string,
url: string,
thumbnailUrl: string,
};
const cardVariants: Variants = {
unloaded: {
y: -100,
opacity: 0,
transition: {
when: "afterChildren",
},
},
loaded: {
y: 0,
opacity: 1,
transition: {
when: "beforeChildren",
},
}
};
const textVariants: Variants = {
unloaded: {
scale: .7,
opacity: 0,
},
loaded: {
scale: 1,
opacity: 1,
},
};
const Picture: React.FC<PictureProps> = ({
id, title, url,
albumId, thumbnailUrl
}) => {
console.log(`img-${id}`);
return (
<motion.div className='card' variants={cardVariants} initial="unloaded" animate="loaded" exit="exit">
<div className="card-image">
<motion.figure
className="image"
layoutId={`img-${id}`}
>
<img src={url} alt={title} />
</motion.figure>
</div>
<motion.div className="card-content">
<div className="media">
<div className="media-left">
<Link href={`images/${id}`}>
<a>
<figure className="image is-50by50">
<img src={thumbnailUrl} alt="thumbnail" />
</figure>
</a>
</Link>
</div>
<motion.div variants={textVariants} className="media-content">
<p className="title is-6">{title}</p>
</motion.div>
</div>
<motion.div variants={textVariants} className='content'>
Some random text Some random text
Some random text Some random text
<br />
<strong>Album id</strong>: <em>{albumId}</em>
</motion.div>
</motion.div>
</motion.div>
);
}
export default Picture;