Mark Funkhouser
Mark Funkhouser

Reputation: 43

Problem with intersection oberserver api / useEffect hook triggering callback twice

I'm not exactly certain why but something i've done is causing the callback in my intersectionObserver instance to fire 2 times when it should only fire 1 time. any insights as to why that might be would be greatly appreciated.

amongst other things happening in this component, I'm trying to set up insersectionObserver to observe an element i'vecalled bottom-boundary, by calling the observe method on my observer instance inside a useEffect() hook. this works fine, except that the observer fires my callback twice. I've tried adding the observer to the dependancy array, and I've tried adding getEvents to the dependancy array, but I just an infinite loop instead of a double fire.

import React, { useState, useEffect, useReducer } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import CreatePromo from './components/promo/CreatePromo';
import MainStyle from './components/MainStyle';
import Home from './components/Home';
import SearchEvents from './components/SearchEvents';
import Social from './components/Social';
import SimpleExample from './components/EventLocationMap';
import PromoState from './components/context/PromoContext/PromoState';
import './tailwind.generated.css';
import 'react-quill/dist/quill.snow.css';
import './App.css';
// @TODO start using context api for global state management ASAP.

const App = () => {
  //set initial createPromo widget state.
  const [allEvents, setAllEvents] = useState([]);
  const [promoState, setPromoState] = useState({
    name: '',
    type: '',
    startDate: '',
    startTime: '',
    endDate: '',
    endTime: '',
    address: '',
    city: '',
    state: '',
    postal: '',
    description: '',
    pictures: '',
    files: null,
  });

  // options for the IntersectionObserver constructor below
  let options = {
    root: null,
    rootMargin: '0px',
    threshold: 1.0,
  };

  let eventsToShow = 0;

  // instantiate intersection observer.
  const observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      console.log(entry);
      console.log(entry.isIntersecting);
    });
    eventsToShow += 4;
    console.log('trigger');
    getEvents(eventsToShow);
  }, options);

  //defines our backend api call to get events
  const getEvents = async (numberOfEvents) => {
    try {
      const events = await fetch(
        `http://localhost:3001/api/events/${numberOfEvents}`
      );
      const newData = await events.json();
      setAllEvents([...allEvents, ...newData]);
      return newData;
    } catch (error) {
      console.log(error);
    }
  };

  //wrapper function to add a new event to allEvents
  const handleSetAllEvents = (newEvent) => {
    setAllEvents([...allEvents, newEvent]);
  };

  //wrapper function for updating controlled form state
  const handlePromoStateChange = (e) => {
    setPromoState({ ...promoState, [e.target.name]: e.target.value });
  };

  //wrapper function for handling the react-quill rich-text input specifically
  const handleDescriptionChange = (value) =>
    setPromoState({ ...promoState, description: value });

  useEffect(() => {
    //loads more events when viewport intersects with #bottom-boundary
    observer.observe(document.querySelector('#bottom-boundary'));
  }, []);

  return (
    // <PromoState>
    <Router>
      <MainStyle>
        <Switch>
          <Route exact path='/'>
            <Home allEvents={allEvents} />
          </Route>
          <Route exact path='/create'>
            <CreatePromo
              promoState={promoState}
              handlePromoStateChange={handlePromoStateChange}
              handleDescriptionChange={handleDescriptionChange}
              handleSetAllEvents={handleSetAllEvents}
            />
          </Route>
          <Route path='/search'>
            <SearchEvents />
          </Route>
          <Route path='/social'>
            <Social />
          </Route>
        </Switch>
      </MainStyle>
    </Router>
    // </PromoState>
  );
};

export default App;

Upvotes: 0

Views: 5023

Answers (2)

Mark Funkhouser
Mark Funkhouser

Reputation: 43

I figured it out.

my callback was being fired when the bottom-boundary element was entering and exiting the viewport. easily solved with a condition inside the forEach loop checking for the value of entry.isIntersecting, and only firing my callback off when isIntersecting === true.

Upvotes: 1

Hadeeb Farhan
Hadeeb Farhan

Reputation: 106

You are creating a new observer on each update. Thats why adding observer to dependancy array ends up in an infinite loop.

Try moving creating IntersectionObserver and related functions to useEffect. This avoids creating a new observer on each update.

Also, you might want to unobserve/destroy the IntersectionObserver on effect cleanup.

It's not easy to say why your callbacks are fired twice without seeing the output. It would be easier if you could attach a minimum reproduction in codesandbox/jsfiddle.

...
useEffect(() => {
let options = {
    root: null,
    rootMargin: '0px',
    threshold: 1.0,
  };

  let eventsToShow = 0;

  // instantiate intersection observer.
  const observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      console.log(entry);
      console.log(entry.isIntersecting);
    });
    eventsToShow += 4;
    console.log('trigger');
    getEvents(eventsToShow);
  }, options);

  //defines our backend api call to get events
  const getEvents = async (numberOfEvents) => {
    try {
      const events = await fetch(
        `http://localhost:3001/api/events/${numberOfEvents}`
      );
      const newData = await events.json();
      setAllEvents(allEvents => [...allEvents, ...newData]);
      return newData;
    } catch (error) {
      console.log(error);
    }
  };
    //loads more events when viewport intersects with #bottom-boundary
    observer.observe(document.querySelector('#bottom-boundary'));

    // destroy observer
    return observer.disconnect;
  }, []);
...

Upvotes: 1

Related Questions