Best way to scroll article out of view and hide it - reactjs

I'm building an article view where you can navigate to the next article by clicking a link in the bottom of the current one. This will trigger the new article being loaded under the current one, the current one will be faded out and then I animate the height of the current article to zero, so you end up with a page transition where the new article is now at the top of the screen.
My problem now is that it will try to maintain my scroll position, so depending on how far I've scrolled, I will start from a different position in the new article. I would like to start from the top. Do I need to use javascript to animate a scroll to the top at the same time or is there an easier way to achieve this?
So far I've added this CSS:
section {
transition: opacity 1s, max-height 1s;
opacity: 1;
max-height: 10000px;
}
.hidden {
opacity: 0;
max-height: 0;
}
Here is a Code Sandbox where you can see the problem in practice.
Adding the .hidden class will trigger content fading out and collapsing. The max-height property is not hardware accelerated, though and the procedure about setting an initial max-height that is always higher than page contents (which I might not know) seems awkward and hacky, though.
Is there a more obvious way to achieve this and always end up being scrolled to the top of the 2nd section when the first one is hidden?

You can use conditional rendering (your animation is lost but u can fix that with framer-motion):
import React, { useState } from "react";
import { motion, AnimatePresence, useAnimation } from "framer-motion";
import "./styles.css";
export default function App() {
const [hideSection, setHideSection] = useState(false);
const controls = useAnimation();
const handleClick = () => {
if (hideSection) {
controls.start({
y: [-1400, 0]
});
} else {
controls.start({
y: [1400, 0]
});
}
setHideSection(!hideSection);
};
return (
<motion.div animate={controls}>
<AnimatePresence>
{!hideSection && (
<motion.section
key="First section"
exit={{ opacity: -1 }}
transition={{ default: { duration: 1, delay: 0 } }}
>
First section
</motion.section>
)}
</AnimatePresence>
<motion.section>
Second section{" "}
<button onClick={handleClick}>Toggle first section</button>
</motion.section>
</motion.div>
);
}

Related

Why y and x axis parameter didn't work for staggerChildren animation in framer motion?

My question is fairly simple but i'm struggling to find the solution. Here i'm trying to create a staggering animation for each letter in a sentence, i want to use the y axis as a parameter to animate but i'm not getting the result i wanted as the sentence fully mounted without animating. But when i tried to use opacity as a parameter, it works exactly fine. What did i do wrong here? Any help would be appreciated :)
// import "./styles.css";
import { motion } from "framer-motion";
export default function App() {
const quotes = "Hello World.";
const parent = {
animate: {
transition: {
staggerChildren: 0.1,
},
},
};
const child = {
initial: { y: 400 },
animate: {
y: 0,
},
};
return (
<div>
<motion.div variants={parent} initial='initial' animate='animate'>
{
quotes.split('').map((item, index) => (
<motion.span variants={child} key={index}>{item}</motion.span>
))
}
</motion.div>
</div>
);
}
To help you see my problem, here is codesandbox example
https://codesandbox.io/s/busy-mountain-fv58xc?file=/src/App.js:0-620
Animating x and y doesn't work for <span> because it's and inline element. It flows with the content and doesn't have an explicit x and y position to animate.
You can change your spans to a block-level element (like div), or you could add some styling to tell the spans to display as blocks:
<motion.span style={{display: "inline-block"}} variants={child} key={index}>
{item}
</motion.span>

How can I add a fade-in animation for Nextjs/Image when it loads?

I'm using next/image, which works great, except the actual image loading in is super jarring and there's no animation or fade in. Is there a way to accomplish this? I've tried a ton of things and none of them work.
Here's my code:
<Image
src={source}
alt=""
layout="responsive"
width={750}
height={height}
className="bg-gray-400"
loading="eager"
/>
According to the docs I can use the className prop, but those are loaded immediately and I can't figure out any way to apply a class after it's loaded.
I also tried onLoad, and according to this ticket, it isn't supported:
https://github.com/vercel/next.js/issues/20368
NextJS now supports placeholder. You can fill the blurDataURL property with the base64 string of the image which you can easily get using the lib plaiceholder on getServerSideProps or getStaticProps. Then to make the transition smoothly you can add transition: 0.3s;
Quick sample:
export const UserInfo: React.FC<TUserInfo> = ({ profile }) => {
return (
<div className="w-24 h-24 rounded-full overflow-hidden">
<Image
src={profile.image}
placeholder="blur"
blurDataURL={profile.blurDataURL}
width="100%"
height="100%"
/>
</div>
);
};
export async function getServerSideProps(props: any) {
const { username } = props.query;
const userProfileByName = `${BASE_URL}/account/user_profile_by_user_name?user_name=${username}`;
const profileResponse = await (await fetch(userProfileByName)).json();
const profile = profileResponse?.result?.data[0];
const { base64 } = await getPlaiceholder(profile.profile_image);
return {
props: {
profile: {
...profile,
blurDataURL: base64,
},
},
};
}
index.css
img {
transition: 0.3s;
}
======== EDIT ==========
If you have the image in the public folder for ex, you don't need to do the above steps, just statically import the asset and add the placeholder type. NextJS will do the rest. Also, make sure to make good use of the size property to load the correct image size for the viewport and use the priority prop for above-the-fold assets. Example:
import NextImage from 'next/image'
import imgSrc from '/public/imgs/awesome-img.png'
return (
...
<NextImage
src={imgSrc}
placeholder='blur'
priority
layout="fill"
sizes="(min-width: 1200px) 33vw, (min-width: 768px) 50vw, 100vw"
/>
)
I wanted to achieve the same thing and tried to use the onLoad event, therefore. The Image component of nextJs accepts this as prop, so this was my result:
const animationVariants = {
visible: { opacity: 1 },
hidden: { opacity: 0 },
}
const FadeInImage = props => {
const [loaded, setLoaded] = useState(false);
const animationControls = useAnimation();
useEffect(
() => {
if(loaded){
animationControls.start("visible");
}
},
[loaded]
);
return(
<motion.div
initial={"hidden"}
animate={animationControls}
variants={animationVariants}
transition={{ ease: "easeOut", duration: 1 }}
>
<Image
{...p}
onLoad={() => setLoaded(true)}
/>
</motion.div>
);
}
However, the Image does not always fade-in, the onLoad event seems to be triggered too early if the image is not cached already. I suspect this is a bug that will be fixed in future nextJS releases. If someone else finds a solution, please keep me updated!
The solution above however works often, and since onLoad gets triggered every time, it does not break anything.
Edit: This solution uses framer-motion for the animation. This could also be replaced by any other animation library or native CSS transitions
You could try use next-placeholder to achieve this sort of effect
Yes, its possible to capture the event where the actual image loads. I found an answer to this on Reddit and wanted to repost it here for others like me searching for an anwser.
"To get onLoad to work in the NextJS image component you need make sure it's not the 1x1 px they use as placeholder that is the target.
const [imageIsLoaded, setImageIsLoaded] = useState(false)
<Image
width={100}
height={100}
src={'some/src.jpg'}
onLoad={event => {
const target = event.target;
// next/image use an 1x1 px git as placeholder. We only want the onLoad event on the actual image
if (target.src.indexOf('data:image/gif;base64') < 0) {
setImageIsLoaded(true)
}
}}
/>
From there you can just use the imageIsLoaded boolean to do some fadein with something like the Framer Motion library.
Source: https://www.reddit.com/r/nextjs/comments/lwx0j0/fade_in_when_loading_nextimage/

How state of a react hook must update if it has Map?

I have multiple(more than 15) div tags as tiles. I need to emphasis each one if mouse hover on it. So each tag has onMouseEnter/Leave functions as bellow.
<div
key={key}
onMouseEnter={onMouseEnter(key)}
onMouseLeave={onMouseLeave(key)}
>
...
</div>
Also I put each tiles key in a Map data structure.
const onMouseEnter = key => {
return function() {
const newIsHover = new Map(isHover)
newIsHover.set(key, true)
setIsHover(newIsHover)
}
}
const onMouseLeave = key => {
return function() {
const newIsHover = new Map(isHover)
newIsHover.delete(key)
setIsHover(newIsHover)
}
}
Since component is hook it put its state in a useState.
const [isHover, setIsHover] = useState(new Map())
What is happening here:
Always I enter a tile: onMouseEnter function called and its key added to map (as expected)
When I leave a tile: always onMouseLeave called but sometimes key is removed (as expected) and tile turned back to its normal shape but sometimes it does not(problem is here, in this situation map updated at setIsHover in onMouseLeave but it does not changed in the component!).
I think map updated as expected but when I move on new tile it does not understand that yet. So it overwrite it with what it has.
PS: example added. Move between tiles with high speed!
Like the class-based components, calls to update state are asynchronous and get queued up. Try using functional state updates to ensure these queued-up updates correctly update the previous state. This should fix race conditions between quick successive setIsHover calls with the same key.
Notice if you move slowly enough between tiles they correctly highlight and unhighlight, but more quickly (like a swipe) and 2 or more can get stuck until you again slowly exit the tile.
const onMouseEnter = key => {
return function() {
setIsHover(prevIsHover => {
const newIsHover = new Map(prevIsHover);
newIsHover.set(key, true);
return newIsHover;
});
}
}
const onMouseLeave = key => {
return function() {
setIsHover(prevIsHover => {
const newIsHover = new Map(prevIsHover);
newIsHover.delete(key);
return newIsHover;
});
}
}
But I should note that this is a lot of leg work for simply applying some component styling, especially hovering. It could more simply be achieved using CSS.
tileStyles.css
.tile {
background-color: lightgray;
border: 3px solid black;
height: 100px;
line-height: 100px;
margin: 10px;
text-align: center;
width: 100px;
}
.tile:hover {
border-color: red;
}
tile.jsx
import React from "react";
import { withStyles } from "#material-ui/core";
import "./tileStyles.css";
const styles = {
container: { display: "flex", width: "600px", flexWrap: "wrap" }
};
const Tiles = ({ classes: { container }, tiles }) => {
return (
<div className={container}>
{tiles.map((tl, key) => {
return (
<div className="tile" key={key} name={key}>
hi
</div>
);
})}
</div>
);
};
export default withStyles(styles)(Tiles);
The normal and hovered styles are applied together (at the same time) and CSS/html will manage when it hovered or not. The component no longer requires event listeners and doesn't need to maintain internal state.
Explanation
what means "...calls to update state are asynchronous and get queued up."?
When you call this.setState or a useState update function the update doesn't happen synchronously right then and there, but they are queued up during the current render cycle and batch processed in the order in which they were queued. Perhaps this demo will help illustrate what happens. What confounds this issue is the fact that event processing is also asynchronous, meaning that, when events occur their registered callbacks are placed in the event queue to be processed.

Modal component renders Twice on open

I'm using react-spring to animate a Modal based on #reach/dialog. The Modal can have any children. In the children I'm fetching some data based on some prop.
The problem is that the fetch call is made two times on opening the modal. I think it has probably to do with how I'm managing the state and that is causing re-renders.
I'v tried memoizing the children inside the modal and it didn't work, so I think that the problem is outside of the Modal component.
Here is something close to my code and how it is working https://codesandbox.io/s/loving-liskov-1xouh
EDIT: I already know that if I remove the react-spring animation the double rendering doesn't happen, but I want to try keeping the animation intact.
Do you think you can help me to identify where is the bug? (Also some tips on good practice with hooks are highly appreciated).
it renders three times because your return component has transitions.map since you have three item inside the
from: { opacity: 0 }
enter: { opacity: 1 }
leave: { opacity: 0 }
the {children} was called two times when the isOpen is true
you can fix the issue with just removing the from: { opacity: 0 } and leave: { opacity: 0 }
so change your modal.js => transitions
const transitions = useTransition(isOpen, null, {
enter: { opacity: 1 }
});
I checked and it is rendered twice because of animation in a Modal component when an animation is finished, modal is rendered second time when I commented out fragment responsible for animation, Modal renders only once.
const Modal = ({ children, toggle, isOpen }) => {
// const transitions = useTransition(isOpen, null, {
// from: { opacity: 0 },
// enter: { opacity: 1 },
// leave: { opacity: 0 }
// });
console.log("render");
const AnimatedDialogOverlay = animated(DialogOverlay);
// return transitions.map(
// ({ item, key, props }) =>
// item && (
return (
<AnimatedDialogOverlay isOpen={isOpen}>
<DialogContent>
<div
style={{
display: `flex`,
width: `100%`,
alignItems: `center`,
justifyContent: `space-between`
}}
>
<h2 style={{ margin: `4px 0` }}>Modal Title</h2>
<button onClick={toggle}>Close</button>
</div>
{children}
</DialogContent>
</AnimatedDialogOverlay>
);
// )
// );
};
The problem is, that at the end of the animation AnotherComponent remounts. I read similar problems about react-spring. One way could be, that you lift out the state from AnotherComponent to the index.js. This way the state will not lost at remount and you can prevent refetching the data.
const AnotherComponent = ({ url, todo, setTodo }) => {
useEffect(() => {
if (todo.length === 0) {
axios.get(url).then(res => setTodo(res.data));
}
});
....
}
Here is my version: https://codesandbox.io/s/quiet-pond-idyee

How to handle mouse event in stateless component in React

All:
I wonder if I use stateless component, how can I handle mouse event to change component style, for example:
const Com = (props) => {
var hltStyle = false;
function highlight(){
// I do not know what put here
}
var hltStyle = {
backgroundColor: !hltStyle?"lightgreen": "tomato"
}
return (
<div style={hltStyle} onMouseOver={ highlight } >HOVER ME</div>
)
}
What I want just hover this component and change background color. There is some other logic inside highlight, that is why I can not simply use CSS
Thanks
You can achieve that using something like this
const Com = () => {
function over(e){
e.target.style.backgroundColor="red";
}
function out(e){
e.target.style.backgroundColor='';
}
return <div onMouseOver={over} onMouseOut={out}>HOVER ME </div>;
}
Anyway, if you feel that you need to declare some variables to use them as the state, you should use a normal component instead of a stateless one.
jsfiddle
What about using classic CSS for simple hover effects?
<div class="el-to-hover"></div>
somewhere in css-file:
.el-t-hover {
background: transparent
transition: background .3s ease-in-out;
}
.el-to-hover:hover {
background: red
}

Resources