How to prevent double page loading in React/NextJS + Framer Motion - reactjs

I'm trying to animate an image gallery with framer-motion.
I'm using AnimatePresence to detect the value change and animate accordingly.
When I click any links on the same page, for example to a related product, the page renders twice.
Without AnimatePresence it works fine. What am I doing wrong here?
This version, without motion works fine, and causes no errors ( but obvs doesn't animate.. )
// useSate - Change Image
const [currentImage, setCurrentImage] = useState(0);
const galleryImages = [
image_one.url,
image_two.url,
image_three.url,
image_four.url,
]
// Click Handler
<div onClick={ () => { setCurrentImage( 1 ) } }></div>
// Gallery Image Container
<div style={{ backgroundImage: "url(" + galleryImages[currentImage] + ")" }}></div>
This version causes the links on the page to trigger a 'double reload', but the gallery works..
<AnimatePresence>
<motion.div
initial={{ opacity: 0, scale: 1.05 }}
animate={{ opacity: 1, scale: 1 }}
transition={{
duration: .4,
ease: "easeInOut"
}}
style={{ backgroundImage: "url(" + galleryImages[currentImage] + ")" }}
key={ currentImage }
>
</motion.div>
</AnimatePresence>
I've also tried using mode="exit" (/exitBeforeEnter), and useEffect.

Related

Framer-motion: Fire onTap() when click the other button

I have RotatingButton which is turning and changing scale when clicked.
(Rotating has to be slower so I used two motions and added duration to rotate.)
Button is adding +1 to state. ( addValue() )
<div>
<div>
<motion.button
onClick={addValue}
animate={{ rotate: rotation }}
transition={{ duration: 0.5, ease: 'easeIn' }}
>
<motion.div whileTap={{ scale: 0.8 }}>
<RotatingButton />
</motion.div>
</motion.button>
</div>
<div>
<button onClick={removeValue}>{text}</button>
</div>
</div>
But the problem is that I have second button which is removing -1 value from state ( removeValue() ) and it also has to turn and scale the RotatingButton.
Rotating is easy, I added dependency:
const [rotation, setRotation] = useState<number>(0);
and setting rotation on each addValue() and removeValue().
setRotation(rotation + 180);
but the problem is that I couldn't find the way to scale RotatingButton for 0.5 sec for example by triggering whileTap() on when removeValue() is triggered.
I've tried to make scale state and use useEffect nad trigger it when rotation is changing:
useEffect(() => {
setScale(0.8);
setTimeout(() => {
setScale(0);
}, 300);
}, [rotation]);
and I've also tried
const controls = useAnimationControls();
with controls.start, controls.mount etc.
To trigger it other way, with animation to change scale for 0.5 sec when changing the state, skipping whileTap.
I've tried onTapStart() too.
None of that worked.
Please help me.
The way was to use sequence
https://www.framer.com/motion/use-animation-controls/#sequence
const controls = useAnimationControls();
const sequence = async () => {
await controls.start({ scale: 0.8, transition: { duration: 0.5 } });
return await controls.start({ scale: 1, transition: { duration: 0.5 } });
};
and change whileTap() to animate={controls}
<motion.button
onClick={addValue}
animate={{ rotate: rotation }}
transition={{ duration: 0.5, ease: 'easeIn' }}
>
<motion.div animate={controls}>
<RotatingButton />
</motion.div>
</motion.button>

Animating height using Framer Motion not working in React

So, I have been trying to use Framer Motion for my React project. I basically want to animate the height from 0 to "auto", when the div gets rendered.
I tried the below code, but the height is not getting animated
<motion.div
initial={{ height: 0 }}
animate={{ height: "auto" }}
transition={{ duration: 0.5 }}
key={searchQuery?.length}
>
When I replaced height with width, the animation works fine, but can't figure out why the height is not getting animated. And I was unable to find any proper documentation regarding this.
Here is the CodeSandbox Link for demo.
Fixed versinn
What was wrong?
Your conditional rendering logic was in the wrong place, AnimatePresence works only if its direct child disappears.
exit prop was missing
key prop has to be stable, it can't be the length of the string
overflow: hidden have to be added so the children are invisible
Final code:
export default function App() {
const ref = useRef(null);
const [isActive, setIsActive] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<div>
<input
placeholder={"Enter Keyword"}
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
}}
/>
<AnimatePresence>
{searchQuery?.length >= 1 && (
<motion.div
style={{ overflow: "hidden" }}
initial={{ height: 0 }}
animate={{ height: "auto" }}
transition={{ duration: 0.5 }}
exit={{ height: 0 }}
key={"container"}
>
{dataList?.map((listItem) => (
<div
style={{
padding: "1rem",
color: "#E090EE",
borderBottom: "1px solid #E1E1E1",
}}
>
{listItem.activity_title}
</div>
))}
</motion.div>
)}
</AnimatePresence>
</div>
</div>
);
}

Animate input width from right to left framer motion

I'm trying to make my input appears with an animation making its width size going from 0 to 50%. For this, I've used framer-motion. I've managed to make the animation but by default it's growing from the left to the right. Is there any way I could make it change to have it growing from the right to the left ?
sandbox simple reproduction
import { motion } from "framer-motion";
import { useState } from "react";
export default function App() {
const [toggle, setToggle] = useState(false);
return (
<div>
<button onClick={() => setToggle(!toggle)}>toggle input</button>
<div style={{ marginTop: 25 }}>
{toggle && (
<motion.input
initial={{ width: "0%" }}
animate={{ width: "50%" }}
transition={{ duration: 1, origin: 1 }}
/>
)}
</div>
</div>
);
}
Pretty sure it's not the best way of doing this but I think changing the following should fix your problem. Added x with 50vw = 50% of viewport width (0vw = 0% width) for the initial "position" and for animate x with 0 for the ending position as well as width of 50vw.
<motion.input
initial={{ width: "0vw", x: "50vw" }}
animate={{ width: "50vw", x: 0 }}
transition={{ duration: 1, origin: 1 }}
/>
You can find more details about it on the API Documentation of the motion component under Value type conversion https://www.framer.com/docs/component/##value-type-conversion.

Animate background using Framer Motion

I'm trying to create a React component where when scrolling down into it, its background transitions from a color to an image.
This is the component:
const TeamSection = () => {
return (
<Background>
<TeamSectionContainer
initial={{
opacity: 0,
}}
whileInView={{
opacity: 1,
}}
transition={{
ease: "easeOut",
duration: 2,
}}
>
<Title>Text</Title>
<Team>Text<Team>
</TeamSectionContainer>
</Background>
);
};
And the styles:
const TeamSectionContainer = styled(motion.div)`
${tw`
bg-no-repeat
w-full
min-h-[200vh]
flex
flex-col
`};
background-image: url(${BackgroundImage});
`;
const Background = tw.div`
bg-[#001F33]
`;
The result is that th animation works for the whole TeamSectionContainer component, indluding its background. I'm trying to make it so that none of the content is animated and only the background is.
How could be this achieved?
I don't think you can animate the opacity of a background image like that. You'd need to add a separate div inside the TeamSectionContainer with the background image and apply the animation to that:
const TeamSection = () => {
return (
<Background>
<TeamSectionContainer>
<motion.div className="backgroundImage"
initial={{
opacity: 0,
}}
whileInView={{
opacity: 1,
}}
transition={{
ease: "easeOut",
duration: 2,
}}
/>
<Title>Text</Title>
<Team>Text<Team>
</TeamSectionContainer>
</Background>
);
};

Is it possible to animate to 100% in react-spring?

I am using react-spring to try and animate in AJAX content as it is loaded.
I have a container component that I sometimes want to animate to 'auto' from 0 and sometimes I want to animate to 100% depending on a prop that is passed in.
I have a const that I set that is then passed into a calculatedHeight property in the Transition component. I then use this to set the height property in the mounted child component's style property.
const Container = ({ data, children, stretchHeight }) => {
const loaded = data.loadStatus === 'LOADED';
const loading = data.loadStatus === 'LOADING';
const animationHeight = stretchHeight ? '100%' : 'auto';
return (
<div
className={classnames({
'data-container': true,
'is-loading': loading,
'is-loaded': loaded,
'stretch-height': stretchHeight
})}
aria-live="polite"
>
{loading &&
<div style={styles} className='data-container__spinner-wrapper'>
<LoadingSpinner />
</div>
}
<Transition
from={{ opacity: 0, calculatedHeight: 0 }}
enter={{ opacity: 1, calculatedHeight: animationHeight }}
leave={{ opacity: 0, calculatedHeight: 0 }}
config={config.slow}
>
{loaded && (styles => {
return (
<div style={{ opacity: styles.opacity, height: styles.calculatedHeight }}>
{children}
</div>
)
}
)}
</Transition>
</div>
)
}
The problem is that this causes a max callstack exceeded error as I don't think react-spring can understand the '100%' string value, only 'auto'.
Is there a work around for this?
The problem is that you switch types, you go from 0 to auto to 0%. It can interpolate auto, but that gets interpolated as a number, you're going to confuse it by mixing that number with a percentage.
PS. Maybe you can trick a little using css: https://codesandbox.io/embed/xolnko178q
Thanks to #hpalu for helping me realise what the issue was:
The problem is that you switch types, you go from 0 to auto to 0%. It
can interpolate auto, but that gets interpolated as a number, you're
going to confuse it by mixing that number with a percentage.
To resolve this I created consts for both my start and end points.
const containerHeightAnimationStart = stretchHeight ? '0%' : 0;
const containerHeightAnimationEnd = stretchHeight ? '100%' : 'auto';
I then used these in the animation:
<Transition
native
from={{ opacity: 0, height: containerHeightAnimationStart }}
enter={{ opacity: 1, height: containerHeightAnimationEnd }}
leave={{ opacity: 0, height: containerHeightAnimationStart }}
>
{loaded && (styles => {
return (
<animated.div style={styles}>
{children}
</animated.div>
)
}
)}
</Transition>
from & to need same unit (number or string)
const [percentage, setPercentage] = useState(100);
// wrong
const animationState2 = useSpring({
from:{width: 0},
to: {width: `${percentage}%`}
});
// right
const animationState2 = useSpring({
from:{width: '0%'},
to: {width: `${percentage}%`}
});

Resources