React Link framer motion animation with AnimatePresence - reactjs

I have a Navigation component in which the Menu Items float in separately on load and float out on click.
When I added Router and changed the items to Links, the exit animation didn't work because it loaded the new Route component right away.
I want to keep the items individual animation with Link functionality.
Here is the link:
https://codesandbox.io/s/elastic-leaf-fxsswo?file=/src/components/Navigation.js
Code:
export const Navigation = () => {
const navRef = useRef(null);
const onResize = () => {
setIsColumn(window.innerWidth <= 715);
};
const [clickOnMenu, setClick] = useState(false);
const [itemtransition, setTransition] = useState(
Array(menuItems.length).fill(0)
);
const [isColumn, setIsColumn] = useState(window.innerWidth <= 715);
const click = (e) => {
const copy = [...itemtransition];
const index = e.target.id;
setTransition(copy.map((e, i) => (Math.abs(index - i) + 1) / 10));
setTimeout(() => setClick(true), 50);
};
useEffect(() => {
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
return (
<AnimatePresence exitBeforeEnter>
{!clickOnMenu && (
<Nav ref={navRef}>
{menuItems.map((e, i) => {
const text = Object.keys(e)[0];
const value = Object.values(e)[0];
return (
<Item
id={i}
key={value}
animate={{
x: 0,
y: 0,
opacity: 1,
transition: { delay: (i + 1) / 10 }
}}
initial={{
x: isColumn ? 1000 : 0,
y: isColumn ? 0 : 1000,
opacity: 0
}}
exit={{
x: isColumn ? -1000 : 0,
y: isColumn ? 0 : -1000,
opacity: 0,
transition: { delay: itemtransition[i] }
}}
onClick={click}
>
{/*<Link to={`/${value}`}>{text}</Link>*/}
{text}
</Item>
);
})}
</Nav>
)}
</AnimatePresence>
);
};
In the sandbox in Navigation.js 69-70. row:
This is the desired animation.
69. {/*<Link to={`/${value}`}>{text}</Link>*/}
70. {text}
But when I use Link there is no exit animation
69. <Link to={`/${value}`}>{text}</Link>
70. {/*text*/}
Is there a workaround or I should forget router-dom.
Thank you in forward!

This may be a bit hackish, but with routing and transitions sometimes that is the nature. I suggest rendering the Link so the semantic HTML is correct and add an onClick handler to prevent the default navigation action from occurring. This allows any transitions/animations to go through. Then update the click handler of the Item component to consume the link target and issue an imperative navigation action on a timeout to allow transitions/animations to complete.
I used a 750ms timeout but you may need to tune this value to better suit your needs.
Example:
...
import { Link, useNavigate } from "react-router-dom";
...
export const Navigation = () => {
const navRef = useRef(null);
const navigate = useNavigate(); // <-- access navigate function
...
const click = target => (e) => { // <-- consume target
const copy = [...itemtransition];
const index = e.target.id;
setTransition(copy.map((e, i) => (Math.abs(index - i) + 1) / 10));
setTimeout(() => {
setClick(true);
}, 50);
setTimeout(() => {
navigate(target); // <-- navigate after some delta
}, 750);
};
...
return (
<AnimatePresence exitBeforeEnter>
{!clickOnMenu && (
<Nav ref={navRef}>
{menuItems.map((e, i) => {
const text = Object.keys(e)[0];
const value = Object.values(e)[0];
return (
<Item
...
onClick={click(`/${value}`)} // <-- pass target to handler
>
<Link
to={`/${value}`}
onClick={e => e.preventDefault()} // <-- prevent link click
>
{text}
</Link>
</Item>
);
})}
</Nav>
)}
</AnimatePresence>
);
};
...

Related

why the change of variant is not captured in framer motion?

I am trying to perform an animation fadein and fadeout to the images in a gallery with framer motions variants. The problem is I didn't manage to trigger the second animation.
So far, I have created a holder for the animation to be displayed till the image load, which I call DotLottieWrapper. I trigger the animation of this holder in the first render and when the loading state changes but the second looks like is not happening. My code is bellow:
const [loading,setLoading] = useState(1);
const loadedDone = () => {
const animationTime = 6000
async function wait() {
await new Promise(() => {
setTimeout(() => {
setLoading(0)
}, animationTime);
})
};
wait();
}
const StyledWrapper = wrapperStyle ?
styled(wrapperStyle)`position:relative;` :
styled(imageStyle)`
${cssImageStyle(src)}
`;
const animationVariants = {
0:{
backgroundColor:'#300080',
opacity:0,
transition:{duration:1.5}
},
1:{
opacity:1,
backgroundColor:'#702090',
transition:{duration:1.5}
}
}
return (
<StyledWrapper
as={NormalDiv}
onClick={handleClick || function () { }}
>
{(loading < 2) && (
<DotLottieWrapper
initial='0'
animate={loading===1?'1':'0'}
variants={animationVariants}
key={IDGenerator()}
>
</DotLottieWrapper>
)}
{(loading < 2) && (
<img
key={IDGenerator()}
onLoad={() => {
loadedDone();
}}
src={src}
/>
)}
</StyledWrapper>
)
Have I done something wrong?

React clickable elements in touch container

I'm trying to set up a swipeable container with clickable elements. The component is the following:
const SectionBar = (): JSX.Element => {
const [translatePercent, setTranslatePercent] = useState(0);
const [x0, setX0] = useState(0);
const [isMediumOrLarger, setIsMediumOrLarger] = useState(false);
useEffect(() => {
const mediaQuery = window.matchMedia("(min-width: 768px)");
setIsMediumOrLarger(mediaQuery.matches);
mediaQuery.addEventListener("change", (event) =>
setIsMediumOrLarger(event.matches)
);
}, []);
const setSnapPercent = (index: number) => {
console.log(index);
if (index === 3) {
setTranslatePercent(index * 10 + 6);
} else if (index >= 4) {
setTranslatePercent((index + 1) * 10);
} else {
setTranslatePercent(index * 10);
}
};
const swipe: TouchEventHandler<HTMLUListElement> = (
event: TouchEvent<HTMLUListElement>
) => {
const dx = -(event.changedTouches[0].clientX - x0) / 5;
const percent = Math.max(0, Math.min(dx, MAX_PERCENT));
setTranslatePercent(percent);
};
return (
<nav className={classes["section-bar"]}>
<ul
className={classes["section-list"]}
style={
isMediumOrLarger
? {}
: {
transform: `translateX(-${translatePercent}%)`,
}
}
onTouchStart={(event) => setX0(event.changedTouches[0].clientX)}
onTouchEnd={swipe}
onTouchMove={swipe}
>
{Object.entries(createSections).map(([key, value], index) => (
<li key={key}>
<Link href={`/create/race#`}>
<a className={classes.link} onClick={() => setSnapPercent(index)}>
{value}
</a>
</Link>
</li>
))}
</ul>
</nav>
);
};
When I try testing the component out in the simulated phone in Chrome developer tools I can swipe just fine, but tapping on one of the links only seems to register clicks on the links at index 0 and sometimes 1 regardless of which link I press. When I don't pass the touch event props to the ul the links work just fine, so I think the problem has something to do with the touch events.
I got it working. in swipe, I subtracted the change in x from the current transform percent. I have no idea why that fixed it.

Testing UI with conditional rendering based on screen size

I'm using react-testing-library with jest to test storybook stories. I have a component that, based on it's scrollWidth and the clientWidth it conditionally renders a button to page through the component:
function Collection({
children,
scrollOffset = DEFAULT_SCROLL_X_OFFSET,
onScroll,
...rest
}: HorizontalChipCollectionProps) {
const [scrollX, setScrollX] = useState(0); // For detecting start scroll postion
const [showRightPager, setShowRightPager] = useState(false); // For detecting end of scrolling
const scrollableAreaRef = useRef();
const scrollableAreaCallback = React.useCallback((node) => {
scrollableAreaRef.current = node;
/** Check to see if the scrollWidth is less than the full width, if so hide the right arrow */
if (node?.scrollWidth <= node?.clientWidth) {
setShowRightPager(true);
}
}, [setShowRightPager]);
const leftPageOnClick = () => pagerOnClick(PAGER_DIRECTION.LEFT);
const rightPageOnClick = () => pagerOnClick(PAGER_DIRECTION.RIGHT);
return (
<Box>
<ScrollableArea
py={{
base: 'xsmall',
medium: 'small',
}}
display="flex"
ref={scrollableAreaCallback}
onScroll={scrollCurrCheck}
overflowX="auto"
css={css`
> *:not(:last-child) {
margin-right: var(--space-small);
}
`}
{...rest}
>
{children}
</ScrollableArea>
{!showRightPager ? <Pager data-testid="right-pager" variant="right" onClick={rightPageOnClick} /> : null}
</Box>
);
}
I'm trying to test the rightPageOnClick to confirm it works but I'm unable to find the element in my test. Here is my test:
test('should show pager buttons', () => {
window.innerWidth = 1440;
window.innerHeight = 900;
window.dispatchEvent(new Event('resize'));
render(<Basic />);
const rightPager = screen.getByTestId('right-pager');
expect(rightPager).toBeInTheDocument();
});
Let me know if you need any more info from me!

Why my setInterval() is speeding up inside my React image slider?

Why my interval is speeding up?
When I press any of my buttons NextImage() or PrevImage() my interval starts speeding up and the image starts glitching. Any advice or help?
Here's my code =>
//Image is displayed
const [image, setImage] = React.useState(1);
let imageShowed;
if (image === 1) {
imageShowed = image1;
} else if (image === 2) {
imageShowed = image2;
} else if (image === 3) {
imageShowed = image3;
} else {
imageShowed = image4;
}
// Auto change slide interval
let interval = setInterval(
() => (image === 4 ? setImage(1) : setImage(image + 1)),
5000
);
setTimeout(() => {
clearInterval(interval);
}, 5000);
// Change image functionality
const ChangeImage = (index) => {
setImage(index);
};
/ /Next image
const NextImage = () => {
image === 4 ? setImage(1) : setImage(image + 1);
};
// Previous image
const PrevImage = () => {
image === 1 ? setImage(4) : setImage(image - 1);
};
When you need to have some logic which is depend on changing a variable, it's better to keep those logic inside useEffect
const interval = useRef(null);
const timeout = useRef(null);
useEffect(() => {
interval.current = setInterval(
() => (image === 4 ? setImage(1) : setImage((i) => i + 1)),
5000
);
timeout.current = setTimeout(() => {
clearInterval(interval.current);
}, 5000);
return () => {
clearInterval(interval.current);
clearTimeout(timeout.current);
}
}, [image]);
one point to remember is that if you use a variable instead of using useRef it can increase the possibility of clearing the wrong instance of interval or timeout during the rerenders. useRef can keep the instance and avoid any unwanted bugs
Your approach causes so many problems and you should learn more about react (watch youtube tutorials about react), I did make a working example slider hope to help you and people in the future:
let interval;
const images = [
"https://picsum.photos/300/200?random=1",
"https://picsum.photos/300/200?random=2",
"https://picsum.photos/300/200?random=3",
"https://picsum.photos/300/200?random=4",
"https://picsum.photos/300/200?random=5",
];
const App = () => {
const [slide, setSlide] = React.useState(0);
React.useEffect(() => {
interval = setInterval(() => {
NextSlide();
clearInterval(interval);
}, 5000);
return () => {
clearInterval(interval);
};
}, [slide]);
const ChangeSlideDots = (index) => {
setSlide(index);
};
const NextSlide = () =>
setSlide((prev) => (slide === images.length - 1 ? 0 : prev + 1));
const PrevSlide = () =>
setSlide((prev) => (slide === 0 ? images.length - 1 : prev - 1));
return (
<div style={styles.root}>
<img style={styles.imageDiv} src={images[slide]} />
<button style={styles.buttons} onClick={PrevSlide}>
◁
</button>
<div style={styles.dotDiv}>
{images.map((_, i) => (
<div
key={i}
style={i === slide ? styles.redDot : styles.blackDot}
onClick={() => ChangeSlideDots(i)}
>
.
</div>
))}
</div>
<button style={styles.buttons} onClick={NextSlide}>
▷
</button>
</div>
);
}
const styles = {
root: {
display: "flex",
position: "relative",
width: 300,
height: 200,
},
buttons: {
backgroundColor: "rgb(255 255 255 / 37%)",
border: "none",
zIndex: 2,
flex: 1,
},
imageDiv: {
position: "absolute",
zIndex: 1,
width: 300,
height: 200,
},
dotDiv: {
flex: 10,
zIndex: 2,
fontSize: "30px",
display: "flex",
justifyContent: "center",
},
redDot: {
cursor: "pointer",
color: "red",
},
blackDot: {
cursor: "pointer",
color: "black",
},
};
ReactDOM.render(<App />, document.getElementById("react"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Anytime that you rerender your component, you will run the whole function once. So you will set an interval every time you use setImage(). In order to prevent this, you have to use side effect functions. here you should use useEffect() because you have a functional component. in order to make useEffect() only run once, you have to pass an empty array for dependecy array; So your useEffect will act like componentDidMount() in class components. try the code below:
let interval = null
useEffect(() => {
interval = setInterval(
() => (image === 4 ? setImage(1) : setImage(image + 1)),
5000
)
setTimeout(() => {
clearInterval(interval);
}, 5000)
}, [])
Thanks, everybody for your great answers appreciated a lot your time and help!
So, my final solution looks like this:
const images = [image1, image2, image3, image4];
const quotes = [
'Jobs fill your pockets, adventures fill your soul',
'Travel is the only thing you buy that makes you richer',
'Work, Travel, Save, Repeat',
'Once a year, go someplace you’ve never been before',
];
const App = () => {
//Image is displayed
const [image, setImage] = React.useState(0);
// Auto change slide interval
useEffect(() => {
return () => {
clearInterval(
setInterval((interval) => {
image === 3 ? setImage(1) : setImage(image + 1);
clearInterval(interval.current);
}, 5000)
);
};
}, [image]);
// Change image functionality
const ChangeImage = (index) => {
setImage(index);
};
//Next image
const NextImage = () => {
image === 3 ? setImage(1) : setImage(image + 1);
};
// Previous image
const PrevImage = () => {
image === 1 ? setImage(3) : setImage(image - 1);
};
return (
<Section>
<div className='slideshow-container'>
<div>
<img className='slider_image' src={images[image]} alt='slider' />
<h1 className='slider_title'>{quotes[image]}</h1>
</div>
<button className='slider_prev' onClick={PrevImage}>
❮
</button>
<button className='slider_next' onClick={NextImage}>
❯
</button>
</div>
<div>
<div>
{images.map((image, i) => (
<img
key={i}
alt={`slider${i}`}
src={image}
className='bottom_image'
onClick={() => ChangeImage(i)}
></img>
))}
</div>
</div>
</Section>
);
};

Want a button to trigger the drag animation (translateX) for cards example

It is regarding the following example:
https://codesandbox.io/s/cards-forked-4bcix?file=/src/index.js
I want a tinder like functionality where I can trigger the same transition as drag.
I am trying to add like and dislike button functionality like tinder, but since the buttons are not part of the useSprings props loop, it is hard to align which card I should transform. I want the like or dislike button to communicate with useDrag to trigger a drag, I have tried toggling by useState and passing as an argument of useDrag, and onClick handler on button which set(x:1000, y:0) but that removes all of the cards.
Spent a whole day figuring it out, and I need to deliver things very soon, help will be great please!
Below is the code, and I am using Next.js
import React, { useState, useRef } from "react";
import { useSprings, animated } from "react-spring";
import { useDrag } from "react-use-gesture";
const Try: React.SFC = () => {
const cards = [
"https://upload.wikimedia.org/wikipedia/en/5/53/RWS_Tarot_16_Tower.jpg",
"https://upload.wikimedia.org/wikipedia/en/9/9b/RWS_Tarot_07_Chariot.jpg",
"https://upload.wikimedia.org/wikipedia/en/d/db/RWS_Tarot_06_Lovers.jpg",
// "https://upload.wikimedia.org/wikipedia/en/thumb/8/88/RWS_Tarot_02_High_Priestess.jpg/690px-RWS_Tarot_02_High_Priestess.jpg",
"https://upload.wikimedia.org/wikipedia/en/d/de/RWS_Tarot_01_Magician.jpg",
];
const [hasLiked, setHasLiked] = useState(false);
const [props, set] = useSprings(cards.length, (i) => ({
x: 0,
y: 0,
}));
const [gone, setGone] = useState(() => new Set()); // The set flags all the cards that are flicked out
const itemsRef = useRef([]);
const bind = useDrag(
({
args: [index, hasLiked],
down,
movement: [mx],
distance,
direction: [xDir],
velocity,
}) => {
const trigger = velocity > 0.2;
const dir = xDir < 0 ? -1 : 1;
if (!down && trigger) gone.add(index); // If button/finger's up and trigger velocity is reached, we flag the card ready to fly out
set((i) => {
if (index !== i) return; // We're only interested in changing spring-data for the current spring
const isGone = gone.has(index);
const x = isGone ? (200 + window.innerWidth) * dir : down ? mx : 0; // When a card is gone it flys out left or right, otherwise goes back to zero
const rot = mx / 100 + (isGone ? dir * 10 * velocity : 0); // How much the card tilts, flicking it harder makes it rotate faster
const scale = down ? 1.1 : 1; // Active cards lift up a bit
return {
x,
rot,
scale,
delay: undefined,
config: { friction: 50, tension: down ? 800 : isGone ? 200 : 500 },
};
});
if (!down && gone.size === cards.length)
setTimeout(() => gone.clear(), 600);
}
);
console.log(gone);
function handleLikeButtonClick(e) {
gone.add(1);
}
function handleDisLikeButtonClick(e) {
set({ x: -1000, y: 0 });
}
return (
<>
<div id="deckContainer">
{props.map(({ x, y }, i) => (
<animated.div
{...bind(i, hasLiked)}
key={i}
style={{
x,
y,
}}
ref={(el) => (itemsRef.current[i] = el)}
>
<img src={`${cards[i]}`} />
</animated.div>
))}
</div>
<div className="buttonsContainer">
<button onClick={(e) => handleLikeButtonClick(e)}>Like</button>
<button onClick={(e) => handleDisLikeButtonClick(e)}>Dislike</button>
</div>
<style jsx>{`
.buttonsContainer {
background-color: tomato;
}
#deckContainer {
display: flex;
flex-direction: column;
align-items: center;
width: 100vw;
height: 100vh;
position: relative;
}
`}</style>
</>
);
};
export default Try;
done needed to be smart with js:
function handleLikeButtonClick(e) {
let untransformedCards = [];
//loop through all cards and add ones who still have not been transformed
[].forEach.call(itemsRef.current, (p) => {
if (p.style.transform == "none") {
untransformedCards.push(p);
}
});
// add transform property to the latest card
untransformedCards[untransformedCards.length - 1].style.transform =
"translate3d(1000px, 0 ,0)";
}
function handleDisLikeButtonClick(e) {
let untransformedCards = [];
[].forEach.call(itemsRef.current, (p) => {
if (p.style.transform == "none") {
untransformedCards.push(p);
}
});
untransformedCards[untransformedCards.length - 1].style.transform =
"translate3d(-1000px, 0 ,0)";
}

Resources