0

I have an application that is essentially a search with dropdown filters. The idea is to make them seamless with no buttons. When a user reaches the bottom of the page, fetchBooks is called again, except this time it adds on to the list instead of replacing it wholesale. Inversely, if a user types something else in or chooses a different dropdown option, the idea is to use all the selections (which are now parts of the state) to construct an API query with params and get a whole new set which can then increase in size as a user traverses it.

Upon render, there are several state variables that are created via useState().

const bookTitle, setBookTitle = useState()
const isFiction, setIsFiction = useState()

I'm using our homegrown component library to implement the dropdowns but they have simple controls - onChange, initialValue - nothing crazy.

I'm using something similar to detect if a user reaches the end of the list:

useEffect(() => {
   const onScroll = function () {
      if (window.innerHeight + window.scrollY >= document.body.offsetHeight) {
        fetchBooks(true) // true indicates we want to add to the list
      }
   }
   window.addEventListener('scroll', onScroll)
   return () => window.removeEventListener('scroll', onScroll)
}, [])

The problem is that whenever fetchBooks is called the second time around, all of the values of dropdowns stored in state are undefined. I could try to modify the code not to identify too much but that'd be a bit of a project.

My question is - does this ring any bells? Why would a function that exists in the same context of one component see stored state variables as undefined?

Edit:

Here's what fetchBooks looks like. I can't copy paste production code but I'll do my best not to skip anything important:

const fetchBooks = (updating=false) {
  setLoadingIndicator(true);

  // if updating is true, add to list with a different `start` in the url
  // otherwise, start from the beginning of DB list 
  if (updating) {
    searchOffset += 100
  } else {
    searchOffset = 0;
  }

  // think stringbuilder
  const fetchUrl = new URL(BASE_URL);

  if (isFiction)
    fetchUrl.searchParams.set('isFiction', true)
  
  if (searchOffset)
    fetchUrl.searchParams.set('start', searchOffset)
  
  // ... few more fields corresponding directly to state variables that "add on" to the URL
  // this works, but only the first time this function runs. `isFiction` is undefined when this function is called later


  fetch(fetchUrl)
    .then(function (response) {
      // handle non-200 code
      return response.json()
    })
    .then(function(data) {
      if (updating) {
        setBookList(oldBookList => [...oldBookList, ...data.books])
      } else {
        setBookList(data.books);
      }
      setLoadingIndicator(false);
    })
    .catch((e) => {
      console.error(e);
      setLoadingIndicator(false);
    })
}

Second time this function runs, fetchUrl defaults to BASE_URL and the state variables from dropdowns (in this case, isFiction) is undefined. This function doesn't seem like it'd be the cause of the problem.

Edit 2:

There is a useEffect hook that looks like this:

useEffect(() => {
  fetchBooks();
}, [isFiction])

Again, pretty innocent unless I'm missing something.

dsp_099
  • 5,801
  • 17
  • 72
  • 128
  • 1
    Can you please share the contents of fetchBooks? – Greg Jun 02 '22 at 23:58
  • @Greg Updated the original question for clarity. `searchOffset` incidentally works fine but that's because as an experiment I made it a `useRef` and not a `useState`. Making all the dropdown values `useRefs` solves this problem but I'm almost certain that's not how you're supposed to do it. – dsp_099 Jun 03 '22 at 01:11
  • Sounds like your effect hook should depend on the dropdown states – Phil Jun 03 '22 at 01:18
  • @Phil the `useEffect` hook does depend on the dropdown states. It's just that when the app starts it runs `fetchBooks` one time so that there's some initial books populated but every dropdown's state is watched by the useEffect which then calls the fetch. That works fine. Problem is when the scroll to the bottom fires the states are undefined - am I missing something here? – dsp_099 Jun 03 '22 at 01:21
  • I meant literally in the dependency array. Right now, your effect hook has no dependencies. Try moving the `fetchBooks` function definition into your effect hook and you'll probably see es-lint warnings about the dependencies – Phil Jun 03 '22 at 01:22
  • @Phil I updated the OP again. Why would the scroll need to be dependent on anything? Should it not have access to state variables? It doesn't depend anything as I don't want to re-run it every single time something changes, do I? – dsp_099 Jun 03 '22 at 01:24
  • I highly recommend moving side-effect functions like your `fetchBooks` out of the component. Not only does it force you to consider its dependencies properly (ie by passing them in as parameters) but it also makes it so much easier to test in isolation and mock in your components. Have it return data instead of having direct access to state – Phil Jun 03 '22 at 01:25
  • @Phil I'm not sure I understand. I need it to re-run the `fetchBooks` when dropdowns change. Where do I put it then? Bigger question is, why doesn't it have access to state variables? It doesn't need to return anything as you said, it's just defining a scroll functionality such that I can detect the browser window hitting the bottom – dsp_099 Jun 03 '22 at 01:28
  • Because `fetchBooks` is defined within your component, it is re-defined every render cycle. You're capturing the first version in your effect hook because it only runs once on mount – Phil Jun 03 '22 at 01:30
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/245280/discussion-between-dsp-099-and-phil). – dsp_099 Jun 03 '22 at 01:33
  • Sorry no. Just try what I suggested and take note of any warnings about dependencies – Phil Jun 03 '22 at 01:34
  • @Phil Which useEffect are you referring to? Are you saying that that the first one which defines a scroll no longer has access to "updated" state variables? I'm not sure of how to write a working solution – dsp_099 Jun 03 '22 at 01:38
  • The only one that was in your question when I made the comment. I found a duplicate that explains the problem better than I can in the comments – Phil Jun 03 '22 at 01:42
  • @Phil That absolutely worked. Thanks! – dsp_099 Jun 03 '22 at 02:07

0 Answers0