13

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:

My Website

And this is the desired effect:

Sweet fade-in effect

How can I achieve the desired effect?

Here is a link to the code sandbox to test it: Code sandbox link

klaurtar1
  • 700
  • 2
  • 8
  • 29

1 Answers1

12

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>
Danziger
  • 19,628
  • 4
  • 53
  • 83
  • I changed setVisible to setVisible(prevValue => prevValue || entry.isIntersecting) in my code and for whatever reason it just stopped the fade in effect all together. The components are always there now with no fade in. Not sure what I am doing wrong – klaurtar1 Jan 05 '20 at 08:15
  • @klaurtar1 Hard to tell without more details. Maybe try comparing your code with the one in the snippet above. As you can see, that on works. Otherwise, try posting your current code in the question too. – Danziger Jan 05 '20 at 14:49
  • thank you for your help. I included a link at the bottom of the original post to a sandbox with the working code. If you could take a look I would appreciate that immensely – klaurtar1 Jan 05 '20 at 15:56
  • @klaurtar1 That code is not working and the `JobList` component, where you use `FadeInSection` I suppose, is missing. I have updated the code here to have something closer to what you have in there, and it's still working, so I suspect the error might be in `JobList`. Also, try removing `translateY(20vh)` and updating only the opacity first. Translating an element will also affect the `IntersectionObserver`. – Danziger Jan 05 '20 at 17:57
  • I updated my code to reflect yours and still the components are remaining stationary. I believe useEffect is setting isVisible state to true when the browser opens so no fade-in is shown to the user in this approach. I have updated my code sandbox to make sure all components are there. Sorry my internet went out and changes didn't save, but it should be working now – klaurtar1 Jan 05 '20 at 19:29
  • 1
    You had `useState(true)` rather than `useState(false)` and `if (entries.isIntersecting)` rather than `if (entries[0].isIntersecting)`. Fixed here: https://codesandbox.io/s/musing-bush-xys5b. – Danziger Jan 05 '20 at 20:10
  • 1
    Thanks for sharing the answer. I've learned a lot @Danziger – Abdelsalam Shahlol Oct 12 '21 at 21:08
  • I have the exact same component, and I'm able to add a fade-in effect. But I used this FadeInSection component on three different pages, and when I go from one page to another, it throws a memory leak error, and when I refresh, the error goes away. Any idea how to fix that? – r121 Aug 03 '22 at 12:28
  • @r007 Sorry, there was a mistake in the code. When unmounting the component, you should call `observer.disconnect()`, not `observer.unobserve()`. – Danziger Aug 03 '22 at 18:25