I am creating a news ticker but am having issues showing it in a continuous loop. As you can see from the code sandbox here there is gaps on both sides of the ticker at the beginning and at the end. Perhaps it isn't possible to do this in pure css? and maybe I need to use js to manipulate the array. Can anyone give me any guidance on the approach I should take?
I am talking about the blue gap on the right and left at the beginning and end of the animation
import styled, { keyframes } from "styled-components";
const customTicker = keyframes`
0% {
transform: translate3d(1000%, 0, 0);
visibility: visible;
}
100% {
transform: translate3d(-1000%, 0, 0);
}
`;
const CryptosWrapper = styled.div`
background: #123962;
overflow: hidden;
`;
const TickerWrap = styled.div`
display: flex;
overflow: hidden;
`;
const CryptoWrapper = styled.div`
color: white;
flex: 1 0 100px;
animation: ${customTicker} 7s infinite;
animation-timing-function: linear;
`;
const Currency = styled.span`
color: #5d81a6;
`;
const Heading = styled.p`
font-size: 12px;
`;
function App() {
const [cryptos, setCryptos] = React.useState(null);
React.useEffect(() => {
fetch("https://api.coincap.io/v2/assets")
.then(res => {
return res.json();
})
.then(res => {
setCryptos(res.data.slice(0, 10));
});
}, []);
return (
<CryptosWrapper>
<TickerWrap>
{cryptos &&
cryptos.map(crypto => {
return (
<CryptoWrapper key={crypto.id}>
<Heading>{crypto.symbol}/USD</Heading>
<p>
{parseFloat(crypto.priceUsd)
.toFixed(2)
.toLocaleString("en-US")}{" "}
<Currency>USD</Currency>
</p>
</CryptoWrapper>
);
})}
</TickerWrap>
</CryptosWrapper>
);
}
Here's a quick solution, which is somewhat of a hard-coded solution. Imo, there are more robust solutions that will be easier to maintain.
The issue with the current implementation is we don't have an infinite supply of content. We need to have an infinite loop of ticker symbols, so the last ticker symbol is immediately followed by the first ticker symbol.
A quick way to achieve this is to duplicate the ticker symbols. Something like:
setCryptos([...res.data.slice(0, 10), ...res.data.slice(0, 10)]);
This gives us a continuous stream of symbols, like:
A B C D E A B C D E
Now we can tweak the animation loop so it restarts at exactly the moment the 2nd starting symbol A reaches the same position as the 1st A, with a small tweak to the keyframes:
const customTicker = keyframes`
0% {
transform: translate3d(0%, 0, 0);
visibility: visible;
}
100% {
transform: translate3d(-1000%, 0, 0);
}
`;
And here's the result:
const customTicker = window.styled.keyframes`
0% {
transform: translateX(0%);
visibility: visible;
}
100% {
transform: translateX(-1000%);
}
`;
const CryptosWrapper = window.styled.div`
background: #123962;
overflow: hidden;
`;
const TickerWrap = window.styled.div`
display: flex;
overflow: hidden;
`;
const CryptoWrapper = window.styled.div`
color: white;
flex: 1 0 100px;
animation: ${customTicker} 7s infinite;
animation-timing-function: linear;
`;
const Currency = window.styled.span`
color: #5d81a6;
`;
const Heading = window.styled.p`
font-size: 12px;
`;
function App() {
const [cryptos, setCryptos] = React.useState(null);
React.useEffect(() => {
fetch("https://api.coincap.io/v2/assets")
.then(res => {
return res.json();
})
.then(res => {
setCryptos([...res.data.slice(0, 10), ...res.data.slice(0, 10)]);
});
}, []);
return (
<CryptosWrapper>
<TickerWrap>
{cryptos &&
cryptos.map(crypto => {
return (
<CryptoWrapper key={crypto.id}>
<Heading>{crypto.symbol}/USD</Heading>
<p>
{parseFloat(crypto.priceUsd)
.toFixed(2)
.toLocaleString("en-US")}{" "}
<Currency>USD</Currency>
</p>
</CryptoWrapper>
);
})}
</TickerWrap>
</CryptosWrapper>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/styled-components/dist/styled-components.min.js"></script>
<div id="root"></div>
As if superfluous. Why don't you use marquee?
Related
I'm trying to do someting like this:
It's a glowing spot follows the pointer
import { useEffect, useState } from "react";
import "./styles.css";
const CUBE_SIZE = 100;
export default function App() {
const [left, setLeft] = useState(window.innerWidth / 2);
const [top, setTop] = useState(window.innerHeight / 2);
const mouseMoveHandler = (event) => {
setLeft(`${event.clientX - CUBE_SIZE / 2}px`);
setTop(`${event.clientY - CUBE_SIZE / 2}px`);
};
useEffect(() => {
document.addEventListener("mousemove", mouseMoveHandler);
return () => {
document.removeEventListener("mousemove", mouseMoveHandler);
};
}, []);
return (
<div className="App">
<div
style={{ width: `${CUBE_SIZE}px`, height: `${CUBE_SIZE}px`, left, top }}
className="glowing-cube"
/>
</div>
);
}
body {
background-color: black;
}
.App {
font-family: sans-serif;
text-align: center;
}
.glowing-cube {
position: relative;
background: linear-gradient(216.89deg, #dc4b5c 6.73%, #5a53ff 81.32%);
filter: blur(32px);
border-radius: 31px;
transform: rotate(135deg);
}
The codesandbox link is below:
https://7h7cpj.csb.app/
Everything looks perfect in Chrome, while it looks bad on Safari:
I found blur is supported since Safari 6 ref
Then what's the problem?
And, how can I do to improve this?
Is there a feasible solution on Safari?
I'm trying to make a toast message component that works like this.
It comes in from outside the right screen, disappears out of the screen after a certain period of time, and the component get deleted after the animation.
It works perfectly when I make a single component. However, when I create multiple toast components, some of them just get removed before the animation starts.
I tried many ways for 3days, but nothing worked... 😭 I would appreciate it if you could check which part is wrong. Help me plz
Toast Contatiner
import React, { useState, useCallback } from 'react';
import ToastItem from './Toast';
import styled from 'styled-components';
type ToastItemInfo = {
content: string | React.ReactNode;
id: string;
};
const ToastContainer = () => {
const [toastItems, setToastItems] = useState<ToastItemInfo[]>([]);
const handleClick = () => {
setToastItems((toastItems) => [
{
content: 'Toasted !',
id: String(Math.random()).substring(2, 8),
},
...toastItems,
]);
};
const removeToast = useCallback((id: string) => {
setToastItems((toastItems) => toastItems.filter((toast) => toast.id !== id));
}, []);
const hasItems = (toastItems: ToastItemInfo[]) => {
return toastItems.length > 0;
};
const mapItems = (toastItems: ToastItemInfo[]) => {
return toastItems.map((toast) => (
<ToastItem key={toast.id} id={toast.id} removeToast={removeToast}>
{toast.content}
</ToastItem>
));
};
return (
<div>
<Button onClick={handleClick}>Toast 🍞</Button>
{hasItems(toastItems) && <Container>{mapItems(toastItems)}</Container>}
</div>
);
};
const Container = styled.div`
position: fixed;
display: flex;
flex-direction: column;
gap: 8px;
top: 0;
right: 0;
margin: 16px;
`;
// .slideout {
// animation: 0.5s linear animate;
// }
// #keyframes animate {
// from {
// transform: translateX(0);
// }
// to {
// transform: translateX(120%);
// }
// }
const Button = styled.button`
width: 132px;
height: 40px;
background: #434253;
color: white;
font-size: 1rem;
border: 2px solid white;
cursor: pointer;
`;
export default ToastContainer;
ToastItem
import React, { memo, useEffect, useRef, useState } from 'react';
import styled, { keyframes } from 'styled-components';
import ProgressBar from './ProgressBar';
interface ToastProps {
id: string;
children: string | React.ReactNode;
removeToast: (id: string) => void;
}
const autoCloseDelay = 3000;
const ToastItem: React.FC<ToastProps> = (props) => {
const { id, removeToast, children } = props;
const [toClose, setToClose] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const removeFn = () => {
removeToast(id);
};
const { current } = ref;
const timeout = setTimeout(() => {
current?.addEventListener('animationend', () => {
removeToast(id);
});
setToClose(true);
}, autoCloseDelay);
return () => {
clearTimeout(timeout);
current?.removeEventListener('animationend', removeFn);
};
}, [id, removeToast]);
return (
<Toast id={id} ref={ref} onClick={() => removeToast(id)} toClose={toClose}>
{children}
<ProgressBar autoCloseDelay={autoCloseDelay} />
</Toast>
);
};
const slideIn = keyframes`
from{
transform: translateX(120%);
}
to{
transform: translateX(0);
}
`;
const slideOut = keyframes`
from{
transform: translateX(0);
}
to{
transform: translateX(120%);
}
`;
const Toast = styled.div<{ toClose: boolean }>`
box-sizing: border-box;
position: relative;
padding: 0 16px;
width: 250px;
height: 58px;
line-height: 58px;
background: #fabb4d;
color: #5f2700;
border-radius: 2px;
cursor: pointer;
animation: 0.5s ease 0s ${(props) => (props.toClose ? slideOut : slideIn)};
&:hover {
transform: scale(1.05);
}
`;
export default memo(ToastItem);
enter image description here
I need help.
I'm trying to generate a random number in this code {number} every given second.
How can i do this?
I tried this, it generates the number randomly but doesn't update it every second.
var number;
(function repeat() {
number = Math.floor((Math.random()*100)+1);
setTimeout(repeat, 1000);
setInterval(repeat, 1000);
})();
import React from 'react'
import styled, { keyframes } from 'styled-components'
class ProgressBar extends React.Component {
render() {
const { text } = this.props
const ProgressContainer = styled.div`
margin-bottom: 25px;
`
const Text = styled.span`
font-size: 17px;
font-family: Poppins;
color: #fff;
`
const Value = styled.span`
font-size: 17px;
font-family: Poppins;
color: #fff;
float: right;
`
const ColorAnimation = keyframes`
0% {background: #04e5e5;}
10% {background: #f37055;}
20% {background: #ef4e7b;}
30% {background: #a166ab;}
40% {background: #5073b8;}
50% {background: #04e5e5;}
60% {background: #07b39b;}
70% {background: #6fba82;}
80% {background: #5073b8;}
90% {background: #1098ad;}
100% {background: #f37055;}
`
const Progress = styled.div`
height: 5px;
border-radius: 2.5px;
margin-top: 10px;
transition: 2s;
animation: ${ColorAnimation} 10s infinite alternate;
`
return(
<ProgressContainer>
<Text>{text}</Text>
<Value>{number}%</Value>
<Progress style={{width: `${number}%`}}></Progress>
</ProgressContainer>
)
}
Thanks
Using the useEffect hook, you can create the needed setInterval and clean up when the component unmounts:
useEffect(() => {
const interval = setInterval(() => {
** code for random number goes here **
}, 1000);
return () => clearInterval(interval);
}, []);
In order to have the component re-render when ever the random number changes, you can utilize the useState hook:
const [randomNumber, setRandomNumber] = useState(null);
Putting it all together:
import React, {useState, useEffect} from "react";
const Container = () => {
const [randomNumber, setRandomNumber] = useState(null)
useEffect(() => {
const interval = setInterval(() => {
setRandomNumber(Math.floor((Math.random()*100)+1))
}, 1000);
return () => clearInterval(interval);
}, []);
return (<div>{randomNumber}</div>)
}
You can see it in action in this JSFiddle.
I am trying to achieve this: https://codesandbox.io/s/framer-motion-2-drag-to-reorder-fc4rt?file=/src/use-position-reorder.js
Basically you can drag a card, place it somewhere in the container and reorder the card based on where you drag/put it from the original place.
The problem is that in my code, you can drag the card but not place it. I tried to change the code so many times but I can't find a way out of it. Below you will find the code where I render the component FavouriteTreeNodeComponent which is a LI(List) nested in FavouriteTreeNodesList a UL(Unordered List) :
// react
import { useRef, useEffect, useCallback, useState } from 'react';
// animation spring
import { animated, Transition, useSpring } from 'react-spring';
import { AnimateSharedLayout } from 'framer-motion';
// styled components
import styled from 'styled-components';
// framer motion
import { motion } from 'framer-motion';
import { usePositionReorder } from './ReorderingModalNodes/use-position-reorder';
// icons
import { MdClose } from 'react-icons/md';
// components
import TemplateItem from './TemplateItem';
import SearchBar from './SearchBar';
import DurationIntervalComponent from './DurationIntervalComponent';
import ContextItem from './ContextItem';
import FavouriteTreeNodeComponent from './FavouriteTreeNodeComponent';
const TimeEntryModal = ({ showModal, setShowModal }) => {
// STATES
const [selected, setSelected] = useState('');
const [reportingTemplates, setReportingTemplates] = useState([]);
const [reportingContexts, setReportingContexts] = useState([]);
// USEREF
const modalRef = useRef();
// ANIMATION MODAL
const animation = useSpring({
config: {
duration: 250,
},
opacity: showModal ? 1 : 0,
transform: showModal ? `translateY(0%)` : `translateY(-100%)`,
});
// CLOSING MODAL FROM BACKGROUND
const closeModal = (e) => {
if (modalRef.current === e.target) {
setShowModal(false);
}
};
// CLOSING MODAL WITH ESCAPE BUTTON
const keyPress = useCallback(
(e) => {
if (e.key === 'Escape' && showModal) {
setShowModal(false);
console.log('I pressed');
}
},
[setShowModal, showModal]
);
useEffect(() => {
document.addEventListener('keydown', keyPress);
return () => document.removeEventListener('keydown', keyPress);
}, [keyPress]);
const variants = {
hidden: {
y: '-100vh',
},
visible: { y: 0, transition: { ease: 'easeInOut', duration: 1 } },
exit: {
y: '-100vh',
},
};
// DATA
const colorsData = [
'#86d8bda1',
'#eb472a83',
'#8fcfe3ad',
'#db8ddfb0',
'#e8e46fc0',
'#e1aa66b2',
];
const List = [
'3000',
'4000',
'5000',
'6000',
'7000',
'8000',
];
// FAVOURITES LIST ANIMATION
const [updatedList, updatePosition, updateOrder] =
usePositionReorder(List);
// FETCHES
useEffect(() => {
getTemplates();
}, []);
const url = 'http://localhost:8080/graphql';
const getTemplates = async () => {
try {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: `query {
reportingTemplates {
name
_id
colorScheme
reportingContexts {
name
_id
}
}
}`,
}),
});
const data = await res.json();
console.log(data);
const dataWithColors = data.data.reportingTemplates.map((obj, i) => ({
...obj,
color: colorsData[i],
}));
setReportingTemplates(dataWithColors);
setSelected(dataWithColors[0].color);
setReportingContexts(dataWithColors[0].reportingContexts);
console.log('data', dataWithColors);
} catch (err) {
console.log('ERROR', err);
}
};
console.log('data state', reportingTemplates);
return (
<Background ref={modalRef} onClick={closeModal}>
<motion.div
variants={variants}
initial="hidden"
animate="visible"
exit="exit"
>
<ModalWrapper>
<ModalContent>
<Nav>
<h3>New Entry Tirsdag 2. Sep </h3>
</Nav>
<ModalColumns>
<ColumnOne>
<TemplatesContainer>
<AnimateSharedLayout>
<Templates>
{reportingTemplates.map((template, index) => (
<TemplateItem
key={template.name}
color={template.color}
title={template.name}
isSelected={selected === template.color}
onClick={() => {
setReportingContexts(
reportingTemplates[index].reportingContexts
);
setSelected(template.color);
}}
/>
))}
</Templates>
</AnimateSharedLayout>
</TemplatesContainer>
<InformationBlock>
<h4>Information</h4>
{reportingContexts.map((context) => (
<ContextItem
key={context.name}
text={context.name}
// isSelected={selected === context.color}
// onClick={() => setSelected(context.color)}
/>
))}
</InformationBlock>
<DurationIntervalComponent />
</ColumnOne>
<ColumnTwo>
<SearchBar />
<Recent>
<h4>Recent</h4>
</Recent>
<Favourites>
<h4>Favourites</h4>
<FavouriteTreeNodesList>
{updatedList.map((treeNode, index) => (
<FavouriteTreeNodeComponent
key={index}
index={index}
text={treeNode}
updateOrder={updateOrder}
updatePosition={updatePosition}
/>
))}
</FavouriteTreeNodesList>
</Favourites>
</ColumnTwo>
</ModalColumns>
</ModalContent>
<CloseModalButton
aria-label="Close Modal"
onClick={() => setShowModal((prev) => !prev)}
/>
<Footer>
<DeleteBlock>
<p> Delete </p>
</DeleteBlock>
<CancelNSaveBlock>
<Cancel>
<p> Cancel </p>
</Cancel>
<SaveNClose>
<p> Save and close </p>
</SaveNClose>
<SaveNAdd>
<p> Save and add another </p>
</SaveNAdd>
</CancelNSaveBlock>
</Footer>
</ModalWrapper>
</motion.div>
</Background>
);
};
// STYLES
const Background = styled.div`
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
position: fixed;
display: flex;
justify-content: center;
align-items: center;
z-index: 999;
`;
const ModalWrapper = styled.div`
width: 80vw;
height: 80vh;
box-shadow: 0 5px 16px rgba(0, 0, 0, 0.2);
background: #fff;
color: #000;
display: flex;
flex-direction: column;
align-items: stretch;
border-radius: 10px;
`;
const CloseModalButton = styled(MdClose)`
cursor: pointer;
position: absolute;
top: 20px;
right: 20px;
width: 32px;
height: 32px;
padding: 0;
z-index: 10;
`;
// MODAL CONTENT
const ModalContent = styled.div`
padding: 20px;
position: relative;
`;
// NAV
const Nav = styled.div``;
const ModalColumns = styled.div`
padding: 10px 0px;
display: flex;
`;
// COLUMN ONE
const ColumnOne = styled.div`
width: 50%;
padding: 20px;
`;
const TemplatesContainer = styled.div``;
const Templates = styled.ul`
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: row;
flex-wrap: initial;
justify-content: center;
`;
const InformationBlock = styled.div`
padding-bottom: 20px;
`;
// COLUMN TWO
const ColumnTwo = styled.div`
width: 50%;
padding: 20px;
`;
const Recent = styled.div`
background-color: #f6f6f9;
border: 1px solid #c5c5c5;
border-radius: 20px;
`;
const Favourites = styled.div`
list-style: none;
`;
const FavouriteTreeNodesList = styled.ul`
position: relative;
width: 100%;
`;
// FOOTER
const Footer = styled.div`
display: flex;
position: fixed;
padding: 0 20px;
bottom: 10px;
/* text-align: center; */
width: 100%;
`;
const DeleteBlock = styled.div`
color: #fb423a;
`;
const CancelNSaveBlock = styled.div`
display: flex;
margin-left: auto;
`;
// const [{name:"cancel", color func: ()=>sdasd }, "sada", "asdasd"]
const Cancel = styled.div`
color: #02b396;
margin-right: 40px;
`;
const SaveNClose = styled.div`
font-weight: bolder;
color: #02b396;
margin-right: 40px;
`;
const SaveNAdd = styled.div`
font-weight: bolder;
color: #02b396;
`;
export default TimeEntryModal;
After that the list FavouriteTreeNodeComponent is rendered in the code below:
import { useState } from 'react';
// styled components
import styled from 'styled-components';
// framer motion
import { motion } from 'framer-motion';
import { useMeasurePosition } from './ReorderingModalNodes/use-measure-position';
const FavouriteTreeNodeComponent = ({
text,
updateOrder,
updatePosition,
index,
}) => {
const [isdragged, setIsDragged] = useState(false);
const itemRef = useMeasurePosition((pos) => updatePosition(index, pos));
return (
<ListContainer>
<motion.div
style={{
zIndex: isdragged ? 2 : 1,
height: text.length * 10,
padding: "5px",
}}
dragConstraints={{
top: 0,
bottom: 0,
}}
dragElastic={1}
layout
ref={itemRef}
onDragStart={() => setIsDragged(true)}
onDragEnd={() => setIsDragged(false)}
animate={{
scale: isdragged ? 1.05 : 1,
}}
onViewportBoxUpdate={(_, delta) => {
isdragged && updateOrder(index, delta.y.translate);
}}
drag="y">{text}
</motion.div>
</ListContainer>
);
};
// STYLES
const ListContainer = styled.li`
height: auto;
gap: 9px 0px;
position: relative;
padding: 10px 0px;
list-style: none;
div {
background-color: white;
border-radius: 5px;
border: 1px solid #c5c5c5;
}
`;
export default FavouriteTreeNodeComponent;
The above code is connected to the file: useMeasurePosition same as the original project:
import { useEffect, useRef } from 'react';
export function useMeasurePosition(update) {
// We'll use a `ref` to access the DOM element that the `motion.li` produces.
// This will allow us to measure its height and position, which will be useful to
// decide when a dragging element should switch places with its siblings.
const ref = useRef(null);
// Update the measured position of the item so we can calculate when we should rearrange.
useEffect(() => {
update({
height: ref.current.offsetHeight,
top: ref.current.offsetTop,
});
});
return ref;
}
and to the file: usePositionReorder:
import { useState, useRef } from 'react';
import { clamp, distance } from 'popmotion';
import { arrayMoveImmutable as move} from 'array-move';
export function usePositionReorder(initialState) {
const [order, setOrder] = useState(initialState);
// We need to collect an array of height and position data for all of this component's
// `Item` children, so we can later us that in calculations to decide when a dragging
// `Item` should swap places with its siblings.
const positions = useRef([]).current;
const updatePosition = (i, offset) => (positions[i] = offset);
// Find the ideal index for a dragging item based on its position in the array, and its
// current drag offset. If it's different to its current index, we swap this item with that
// sibling.
const updateOrder = (i, dragOffset) => {
const targetIndex = findIndex(i, dragOffset, positions);
if (targetIndex !== i) setOrder(move(order, i, targetIndex));
};
return [order, updatePosition, updateOrder];
}
const buffer = 30;
export const findIndex = (i, yOffset, positions) => {
let target = i;
const { top, height } = positions[i];
const bottom = top + height;
// If moving down
if (yOffset > 0) {
const nextItem = positions[i + 1];
if (nextItem === undefined) return i;
const swapOffset =
distance(bottom, nextItem.top + nextItem.height / 2) + buffer;
if (yOffset > swapOffset) target = i + 1;
// If moving up
} else if (yOffset < 0) {
const prevItem = positions[i - 1];
if (prevItem === undefined) return i;
const prevBottom = prevItem.top + prevItem.height;
const swapOffset = distance(top, prevBottom - prevItem.height / 2) + buffer;
if (yOffset < -swapOffset) target = i - 1;
}
return clamp(0, positions.length, target);
};
I am trying to give components a fade-in effect in React when the user scrolls, but I want the fade-in effect to only happen the first time the element moves into the viewport.
Currently, the code I am using causes a fade-in every time the element moves into the viewport, so they are constantly fading in and out.
Here is my fade-in component:
import React, {useState, useRef, useEffect} from 'react';
import './styles/FadeInSection.css';
export default function FadeInSection(props) {
const [isVisible, setVisible] = useState(true);
const domRef = React.useRef();
useEffect(() => {
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => setVisible(entry.isIntersecting));
});
observer.observe(domRef.current);
return () => observer.unobserve(domRef.current);
}, []);
return (
<div ref={ domRef } className={ `fade-in-section ${ isVisible ? 'is-visible' : '' }` }>
{ props.children }
</div>
)
}
And these are the styles I'm using:
.fade-in-section {
opacity: 0;
transform: translateY(20vh);
isibility: hidden;
transition: opacity 0.2s ease-out, transform 0.6s ease-out;
will-change: opacity, visibility;
}
.fade-in-section.is-visible {
opacity: 1;
transform: none;
visibility: visible;
display: flex;
}
Here is my website, which keeps fading components in and out, offering a terrible experience:
And this is the desired effect:
How can I achieve the desired effect?
Here is a link to the code sandbox to test it: Code sandbox link
You only need to call setVisible if entry.isIntersecting is true, so simply replace:
setVisible(entry.isIntersecting);
With:
entry.isIntersecting && setVisible(true);
This way, once an entry has already been marked as visible, it won't be unmarked, even if you scroll back up, so the element goes out of the viewport, and entry.isIntersecting becomes false again.
Actually, you can even call observer.unobserve at that point, as you don't care anymore.
const FadeInSection = ({
children,
}) => {
const domRef = React.useRef();
const [isVisible, setVisible] = React.useState(false);
React.useEffect(() => {
const observer = new IntersectionObserver(entries => {
// In your case there's only one element to observe:
if (entries[0].isIntersecting) {
// Not possible to set it back to false like this:
setVisible(true);
// No need to keep observing:
observer.unobserve(domRef.current);
}
});
observer.observe(domRef.current);
return () => observer.disconnect();
}, []);
return (<section ref={ domRef } className={ isVisible ? ' is-visible' : '' }>{ children }</section>);
};
const App = () => {
const items = [1, 2, 3, 4, 5, 6, 7, 8].map(number => (
<FadeInSection key={ number }>Section { number }</FadeInSection>
));
return (<main>{ items }</main>);
}
ReactDOM.render(<App />, document.querySelector('#app'));
body {
font-family: monospace;
margin: 0;
}
section {
padding: 16px;
margin: 16px;
box-shadow: 0 0 8px rgba(0, 0, 0, .125);
height: 64px;
opacity: 0;
transform: translate(0, 50%);
visibility: hidden;
transition: opacity 300ms ease-out, transform 300ms ease-out;
will-change: opacity, visibility;
}
.is-visible {
opacity: 1;
transform: none;
visibility: visible;
display: flex;
}
<script src="https://unpkg.com/react#16.12.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.12.0/umd/react-dom.development.js"></script>
<div id="app"></div>