Framer motion render component animation on each route change - reactjs

I have a component (Navigation) inside a next.js Layout routed by framer-motion, that renders animation every time I change the route. I want to make it render the animation only the first time I go to the /app page. Tried to use useRef of the component, but it wasn't animating at all. Thanks.
So:
/app - it renders the animation
/app/chats - it renders the animation again
Navigation animation rerender example
Navigation component itself
export const UserNavigation = () => {
return (
<motion.section
initial={{ x: -56 }}
animate={{ x: 0 }}
transition={{bounce: false}}
className="h-[100%] w-14 bg-gray-900"
>
<div className="flex flex-col gap-4 items-center justify-center mt-2">
<Link href="/app/chats">
<button
className={classNames(
isChatsPage ? "bg-gray-700" : "hover:bg-gray-700",
"text-white rounded-lg p-2 transition duration-200"
)}
>
<ChatBubbleBottomCenterTextIcon className="w-6 h-6" />
<span className="sr-only">Chats</span>
</button>
</Link>
</div>
</motion.section>
);
};
Layout
export const UserLayout: FC<{ children: ReactNode }> = ({ children }) => {
return (
<main className="h-screen w-screen flex">
<UserNavigation />
{children}
</main>
);
};
Chats page
export const Chats: NextPage = () => {
return (
<UserLayout>
<motion.section
initial={{ x: -56 }}
animate={{ x: 0 }}
transition={{ bounce: false }}
className="w-72 h-full bg-gray-800"
></motion.section>
</UserLayout>
);
};

I had a similar problem a while ago.
In my case I needed to manage animations on enter and on exit of a route. This post helped me out - https://blog.sethcorker.com/page-transitions-in-react-router-with-framer-motion. The main takeaway was the need to supply a key prop for the Switch child so AnimatePresence (https://www.framer.com/docs/animate-presence/) could reliably do it's thing.
But based on your example it doesn't look like you need the added complexity of AnimatePresence. If that is the case, you might just need to fiddle with your layout and routing to get what you need. Here's an example that might give you some ideas of things to try - https://codesandbox.io/s/so-73692915-714wdk.
Preview of the changes at runtime:

Related

React onClick routing ONLY works SOMETIMES

Here is the video proof.
https://dsc.cloud/leonardchoo/Screen-Recording-2022-03-08-at-17.25.58.mov
I'm running into a mysterious error where I click the button for navigation, "onClick" event is fired but it does not redirect and render the target component.
As you can see in the screenshot, the onClick event is logged, but the redirect does not happen.
I reproduced the situation here in CodeSandbox.
Stack
React TS
Mantine UI
React Router V5
How can I solve this issue?
First thing I noticed in your code was that is is rendering a WrapperPage component around each routed component with the navigation logic. I tried simplifying the WrapperPage code as much as possible.
Steps Taken:
Refactored the header and navbar props into standalone components in case there was issue generating JSX
Wrapped the Switch component in App with a single WrapperPage instead of each routed component
The issue persisted.
I next removed the UnstyledButton from #mantine/core so only the Link components were rendered, and could not reproduce. I then tried vanilla HTML buttons instead of the UnstyledButton and they again reproduced the issue.
So it seems it is an issue with rendering an interactive element (i.e. anchor tag from Link) within another interactive element (i.e. button from UnstyledButton) that is an issue. Swapping the element order, i.e. Link wrapping the UnstyledButton, appears to reduce the issue. I can't seem to reproduce the issue with the DOM structured this way.
Header
const CustomHeader = ({
opened,
setOpened
}: {
opened: boolean;
setOpened: React.Dispatch<React.SetStateAction<boolean>>;
}) => {
const theme = useMantineTheme();
return (
<Header height={70} padding="md">
{/* Handle other responsive styles with MediaQuery component or createStyles function */}
<div style={{ display: "flex", alignItems: "center", height: "100%" }}>
<MediaQuery largerThan="sm" styles={{ display: "none" }}>
<Burger
opened={opened}
onClick={() => setOpened((o) => !o)}
size="sm"
color={theme.colors.gray[6]}
mr="xl"
/>
</MediaQuery>
<Group>
<ThemeIcon variant="light" color="orange">
🎙
</ThemeIcon>
<Text>Mantine AppShell with React Router</Text>
</Group>
</div>
</Header>
);
};
Navbar
const CustomNavbar = ({ opened }: { opened: boolean }) => {
const location = useLocation();
const { classes } = useStyles();
return (
<Navbar
padding="md"
// Breakpoint at which navbar will be hidden if hidden prop is true
hiddenBreakpoint="sm"
// Hides navbar when viewport size is less than value specified in hiddenBreakpoint
hidden={!opened}
// when viewport size is less than theme.breakpoints.sm navbar width is 100%
// viewport size > theme.breakpoints.sm – width is 300px
// viewport size > theme.breakpoints.lg – width is 400px
width={{ sm: 300, lg: 400 }}
>
<Link
to="/dashboard"
className={classes.link}
>
<UnstyledButton
className={
location.pathname === "/dashboard"
? classes.button_active
: classes.button
}
>
<Group>
<ThemeIcon variant="light">
<DashboardIcon />
</ThemeIcon>
<Text size="sm">Dashboard</Text>
</Group>
</UnstyledButton>
</Link>
<Link
to="/new-recording"
className={classes.link}
>
<UnstyledButton
className={
location.pathname === "/new-recording"
? classes.button_active
: classes.button
}
>
<Group>
<ThemeIcon variant="light" color="red">
<RadiobuttonIcon />
</ThemeIcon>
<Text size="sm">New Recording</Text>
</Group>
</UnstyledButton>
</Link>
<Link
to="/calendar"
className={classes.link}
>
<UnstyledButton
className={
location.pathname === "/calendar"
? classes.button_active
: classes.button
}
>
<Group>
<ThemeIcon variant="light" color="orange">
<CalendarIcon />
</ThemeIcon>
<Text size="sm">Calendar</Text>
</Group>
</UnstyledButton>
</Link>
</Navbar>
);
};
WrapperPage
const WrapperPage = ({ children }: Props): JSX.Element => {
const [opened, setOpened] = useState(false);
return (
<AppShell
// navbarOffsetBreakpoint controls when navbar should no longer be offset with padding-left
navbarOffsetBreakpoint="sm"
// fixed prop on AppShell will be automatically added to Header and Navbar
fixed
header={<CustomHeader opened={opened} setOpened={setOpened} />}
navbar={<CustomNavbar opened={opened} />}
>
{children}
</AppShell>
);
};

Error: validateDOMNesting(…): <a> cannot appear as a descendant of <a>, Using AniLink inside AniLink

I am using this code snippet in a Gatsby project, It seems using AniLink inside another AniLink is not allowed, but I can not figure out the solution:
import React from 'react'
import AniLink from "gatsby-plugin-transition-link/AniLink";
const titleStyle = {
fontWeight: "700",
}
const authorLinkStyle = {
color: "#00BCD4"
}
const Author = ({ children, to }) => (
<AniLink style={authorLinkStyle} to={to} className="font-weight-bold" fade>
{children}
</AniLink>
)
const Card = ({ title, description, timeStamp, authorName, slug }) => {
const cardTextColor = typeof window !== "undefined" && getComputedStyle(document.documentElement).getPropertyValue("--card-text-color")
const cardLinkStyle = { color: cardTextColor, textDecoration: "none" }
return (
<AniLink
style={cardLinkStyle}
to={slug}
cover
bg="#00BCD4"
>
<div className="card my-4" >
<div className="card-body" >
<h5 className="card-title" style={titleStyle}>{title}</h5>
<p className="card-text">{description}</p>
<h6 className="card-subtitle text-muted">
<Author to="/about">{authorName}</Author> on {timeStamp}
</h6>
</div>
</div>
</AniLink>)
}
export default Card
I think errors happen from this line:
<Author to="/about">{authorName}</Author> on {timeStamp}
The error in F12:
Have anyone using AniLink and see this error?
Any suggestion would be great..
Thanks
Well, basically you spotted the mistake. You are wrapping a component (AniLink, which is an anchor, <a>) inside another anchor (Author component).
This, when compiled and transpiled into the output HTML, is creating an invalid structure of an anchor as a descendant of another anchor (<a><a>Link</a></a>). Is not an issue of AniLink nor React or Gatsby, is an issue of your component's structure.
The solution is simple, don't wrap it. You may want to do the something like:
<div className="card my-4" >
<div className="card-body" >
<AniLink
style={cardLinkStyle}
to={slug}
cover
bg="#00BCD4"
>
<h5 className="card-title" style={titleStyle}>{title}</h5>
<p className="card-text">{description}</p>
</AniLink>
<h6 className="card-subtitle text-muted">
<Author to="/about">{authorName}</Author> on {timeStamp}
</h6>
</div>
</div>
The snippet above won't create the error but you may need to tweak the layout to fit your specifications and design.

Tailwind conditional transition

I'm creating a collapse/accordion component that shows the content when clicked. I want to add a little delay to that action but what I've doesn't seem to work. Here is what I have:
const Accordion = ({ children, open }) => {
const contentSpace = useRef(null);
return (
<div className="flex flex-col">
<div
ref={contentSpace}
className={clsx(
'overflow-auto overflow-y-hidden h-0 transition-height duration-700 ease-in-out',
{
'h-full': open,
}
)}
>
<div className="py-2">{children}</div>
</div>
</div>
);
};
Any ideas?
Seems like you have to use numeric values in order to make the animation work. So I had to change h-full with a numeric height.

ReactJs/NextJs - Conditionally render mutually exclusive components without compromising load time

I am trying to improve loading speed of a react web app.
I have two component imports - one for mobile and one for desktop (Bad design? I think so):
import Posts from '../components/post/posts';
import PostsMobile from '../components/post/postsMobile';
This was easy for development because I did not have to try hard to make the same component compatible for desktop and mobile.
Then to check screen size and load the appropriate component, I do this:
const largeScreen = useMediaQuery(theme => theme.breakpoints.up('sm'));
...
{largeScreen? (
<Posts />
) :
(
<PostsMobile />
)
}
You can resize the browser here to see the two components load: Link to home page showing the two components
Does <PostsMobile /> get imported only when react sees that its needed OR does it automatically get imported in the beginning no matter what?
Is there a better way to conditionally render mutually exclusive components without compromising load time?
Classic conditional rendering, that's the appropriate way to do it, only one component will be added to the DOM in any case. As a side note it's not the best idea to have two different components for mobile vs desktop view, generally your html structure should be the same and you should use CSS for any layout changes (as per Google's suggestions - https://web.dev/responsive-web-design-basics/)
Check out #artsy/fresnel, they have a very straightforward example showing how to configure Next.js and Gatsby.js to achieve screen-width dependent control in SSR environments
Note: it hardly increases overhead size; I am using it in my portfolio currently in conjunction with tailwindcss and it has proven to be a fantastic tool. Easy to configure and its implementation is straightforward. Here is an example of conditionally rendering the same svg icon four times as a function of screen size to customize styles accordingly (xs (mobile), sm, md, greater than md (desktop))
(1) Create a window-width file in your components directory to configure #artsy/fresnel for global sharing
components/window-width.jsx or components/window-width.tsx
import { createMedia } from '#artsy/fresnel';
const PortfolioMedia= createMedia({
breakpoints: {
xs: 0,
sm: 768,
md: 1000,
lg: 1200,
},
})
// Generate CSS to be injected in the head using a styles tag (pages/_document.jsx or pages/_document.tsx)
export const mediaStyles = PortfolioMedia.createMediaStyle();
export const { Media, MediaContextProvider } = PortfolioMedia;
// https://github.com/artsy/fresnel/tree/master/examples/nextjs
Note, you can customize the breakpoints however you'd like; the following breakpoints are from my portfolio's configuration file. They overlap with Tailwind's breakpoints to keep them playing nicely together
// ...
const PortfolioMedia = createMedia({
breakpoints: {
xs: 0,
sm: 640,
md: 768,
lg: 1024,
xl: 1280
}
});
// ...
(2) Wrap pages/index.jsx or pages/index.tsx with the MediaContextProvider component
// ...
import { MediaContextProvider } from 'components/window-width';
interface IndexProps {
allPosts: Post[];
allAbout: AboutType[];
allBlog: BlogType[];
}
const Index = ({ allPosts, allAbout, allBlog }: IndexProps) => {
const morePosts = allPosts.slice(0);
const moreAbout = allAbout.slice(0);
const moreBlog = allBlog.slice(0);
return (
<Fragment>
<MediaContextProvider>
<Lead />
<Head>
<title>{`${CLIENT_NAME} landing page`}</title>
</Head>
<div className='max-w-cardGridMobile md:max-w-cardGrid my-portfolioH2F grid mx-auto content-center justify-center items-center text-center'>
{morePosts.length > 0 && <Cards posts={morePosts} />}
</div>
<div className='max-w-full my-portfolioH2F block mx-auto content-center justify-center items-center text-left'>
{moreAbout.length > 0 && <AboutCoalesced abouts={allAbout} />}
</div>
<div className='max-w-full my-portfolioH2F block mx-auto content-center justify-center items-center text-left'>
{moreBlog.length > 0 && <BlogCoalesced blogs={allBlog} />}
</div>
<Footer />
</MediaContextProvider>
</Fragment>
);
};
export default Index;
// ...
(3) Finally, inject generated mediaStyles CSS into a style tag of type text/css in Next's Head
pages/_document.jsx or pages/_document.tsx
import Document, {
Html,
Head,
Main,
NextScript,
DocumentContext
} from 'next/document';
import { mediaStyles } from 'components/window-width';
export default class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const initialProps = await Document.getInitialProps(ctx);
return { ...initialProps };
}
render() {
return (
<Html lang='en-US'>
<Head>
<meta charSet='utf-8' />
<link rel='stylesheet' href='https://use.typekit.net/cub6off.css' />
<style type='text/css' dangerouslySetInnerHTML={{ __html: mediaStyles }} />
</Head>
<body className='root'>
<script src='./noflash.js' />
<Main />
<NextScript />
</body>
</Html>
);
}
}
(4) Profit; Configuration complete
That's all there is to it. For the sake of clarity I threw in an actual example from one of my projects below.
(5) Bonus example - conditionally rendering ArIcon as a function of device-size
components/lead-arIcon.tsx
import { ArIcon } from 'components/svg-icons';
import Link from 'next/link';
import { Media } from 'components/window-width';
import { Fragment } from 'react';
import DarkMode from 'components/lead-dark-mode';
const ArIconConditional = (): JSX.Element => {
const arIconXs: JSX.Element = (
<Media at='xs'>
<Link href='/'>
<a
className='container block pl-portfolio pt-portfolio justify-between mx-auto w-full min-w-full '
id='top'
aria-label='top'
>
<ArIcon width='18vw' height='18vw' />
</a>
</Link>
</Media>
);
const arIconSm: JSX.Element = (
<Media at='sm'>
<Link href='/'>
<a
className='container block pl-portfolio pt-portfolio justify-between mx-auto w-full min-w-full '
id='top'
aria-label='top'
>
<ArIcon width='15vw' height='15vw' />
</a>
</Link>
</Media>
);
const arIconMd: JSX.Element = (
<Media at='md'>
<Link href='/'>
<a
className='container block pl-portfolio pt-portfolio justify-between mx-auto w-full min-w-full '
id='top'
aria-label='top'
>
<ArIcon width='12.5vw' height='12.5vw' />
</a>
</Link>
</Media>
);
const arIconDesktop: JSX.Element = (
<Media greaterThan='md'>
<Link href='/'>
<a
className='container block pl-portfolio pt-portfolio justify-between mx-auto w-full min-w-full'
id='top'
aria-label='top'
>
<ArIcon
width='10vw'
height='10vw'
classNames={[
` antialised w-svgIcon max-w-svgIcon transform transition-all`,
' stroke-current',
` fill-primary`
]}
/>
</a>
</Link>
</Media>
);
const DarkModeToggler = (): JSX.Element => (
<div className='pt-portfolio text-customTitle transition-all transform -translate-y-mdmxSocial col-span-4 text-right -translate-x-portfolioPadding'>
<DarkMode />
</div>
);
const ArIconsCoalesced = (): JSX.Element => (
<Fragment>
<div className='relative block justify-between lg:w-auto lg:static lg:block lg:justify-start transition-all w-full min-w-full col-span-2'>
{arIconXs}
{arIconSm}
{arIconMd}
{arIconDesktop}
</div>
</Fragment>
);
return (
<Fragment>
<div className='select-none relative z-1 justify-between pt-portfolioDivider navbar-expand-lg grid grid-cols-6 min-w-full w-full container overflow-y-hidden overflow-x-hidden transform'>
<ArIconsCoalesced />
<DarkModeToggler />
</div>
</Fragment>
);
};
export default ArIconConditional;
You could try to lazy load the components.
posts;
componentDidMount() {
if (largeScreen) {
import('../components/post/posts').then(({ default: Posts }) => {
// ^^^^ make sure it has a default export
this.posts = Posts;
this.forceUpdate();
});
} else {
// here load the other component for lower screens
}
}
Then, inside render:
const Posts = this.posts;
return largeScreen? (
<Posts />
) : (
<PostsMobile />
);
Note: You will have to also add a resize listener, so if the screen reaches certain width, the another component will load and will get rendered.
Note2: If you don't care about SSR - you could either try React.lazy with Suspense: https://en.reactjs.org/docs/code-splitting.html#reactlazy
In my opinion there are very valid usages of Desktop vs. Mobile components, for example, although CSS versions are definitely almost always preferred, it might not make sense for drag and drop components that don't work on mobiles, mobile-only hamburger menus, etc.
React's lazy loading is the way to go (unless you need SSR):
import React, { Suspense } from 'react';
const Posts = React.lazy(() => import('../components/post/posts');
const PostsMobile = React.lazy(() => import('../components/post/postsMobile');
function MainComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
{ largeScreen? ( <Posts />) : <PostsMobile /> }
</Suspense>
</div>
);
}
This will guarantee that the component is only loaded when it is first rendered. Bonus tip: if you change the loading div to an animated loading placeholder, the UI of your application may be more pleasant.
None of the answers were compatible with SSR using Nextjs so I ended up using Dynamic Import feature. Looks very powerful but simple.
https://nextjs.org/docs/advanced-features/dynamic-import
import dynamic from 'next/dynamic'
const Posts = dynamic(() => import('../components/post/posts'),
{ loading: () => <LinearProgress /> });
const PostsMobile = dynamic(() => import('../components/post/postsMobile'),
{ loading: () => <LinearProgress /> });
This saved me a few milliseconds
I am not sure if there is a better option so I hope people will comment.

ReactJS - Fade between 2 different components

I am trying to figure out how to fade between 2 different components with a nice easy 1 second transition. I have played around with SwitchTransition and CSSTransition and TransitionGroup and nothing seems to work quite right for me. It looks like SwitchTransition is what I am looking for but the only examples I can find they are just swapping out text and not whole components.
So here is the code for my two components which are shown conditionally based on state variables. Each component has a button that will swap the state variable values so it will currently swap between the two, but it doesn't look real pretty.
<div className="login col-md-6 mt-5 mx-auto">
{showLogin && (
<LoginForm
onLoginSubmit={onLoginSubmit}
email={email}
setEmail={setEmail}
password={password}
setPassword={setPassword}
showPasswordReset={showPasswordReset}
/>
)}
{showForgotPassword && (
<ForgotPasswordForm
onPasswordResetSubmit={onPasswordResetSubmit}
setShowForgotPassword={setShowForgotPassword}
/>
)}
</div>
You can use react-spring to animate your components, first
import { animated, useTransition } from "react-spring";
then inside your component
const [showLogin, set] = useState(false);
const toggle = () => {
set(!showLogin);
};
const transitions = useTransition(showLogin, null, {
from: { position: "absolute", opacity: 0 },
enter: { opacity: 1 },
leave: { opacity: 0 }
});
and then you will render your components, make sure to replace div with animated.div
<div className="login col-md-6 mt-5 mx-auto">
{transitions.map(({ item, key, props }) =>
item ? (
<LoginForm //animated.div
onLoginSubmit={onLoginSubmit}
email={email}
setEmail={setEmail}
password={password}
setPassword={setPassword}
showPasswordReset={showPasswordReset}
/>
) : (
<ForgotPasswordForm //animated.div
onPasswordResetSubmit={onPasswordResetSubmit}
setShowForgotPassword={toggle}
/>
)
)}
</div>
I made a simple example, you can check it out here

Resources