0

For a school project my group is building a table that's filled with city-data via a database-call. The skeleton of the table-component is as such:

import React, { useState } from 'react' 

function Table(props) {
  const [ cities, setCities ] = useState([])
  const [ pageNum, setPageNum ] = useState(0)

  useEffect(()=> { // this sets up the listener for the infinite scroll
    document.querySelector(".TableComponent").onscroll = () => {
      if (Math.ceil(table.scrollHeight - table.scrollTop) === table.clientHeight) showMoreRows()
    }
    //initial fetch
    fetchData(0)
  },[])

  async function showMoreRows() {
    console.log("Show more rows!")
    await fetchData(pageNum)
  }

  async function fetchData(page) {
    // some code, describing fetching
    // EDIT2 start
    console.log(pageNum)
    // EDIT2 end
    const jsonResponse = await {}// THE RESPONSE FROM THE FETCH
    if(page) {
      setCities([...cities, ...jsonResponse])
      console.log("page is true", page)
      setPageNum(pageNum + 1)
    } else {
      console.log("page is false", page) // this always runs and prints "page is false 0"
      setCities([...cities, ...jsonResponse])
      setPageNum(1)
    }
  }


  return <div className="TableComponent"> { pageNum } 
           <!-- The rest of the component --> 
         </div>
}

The table features an "infinite-scrolling"-feature, so when you scroll to the bottom of the page it prints "Show more rows!" and runs fetchData(pageNum) to get more data. At this point, after the initial fetch, the pageNum-variable should be 1 or more, but for some reason the function acts as if it is 0. I put the pageNum-variable on display in the JSX, and I can see that it is 1, but it still prints out "page is false 0" when ever I run it.

When I try to google the issue, it seems the only similar thing could be that I try to read a useState-variable too soon after using setPageNum (before the redraw), but that isn't the case here as far as I can see. I give it plenty of time between tries, and it always says pageNum is zero.

Any ideas as to what I am doing wrong, and how this makes sense in any way?

Thanks for any help!

EDIT: Just tried the code I wrote over, and it seemed to work - however the full code I have doesn't work. Anyone have any ideas about problems related to this, even if the above code might work?

EDIT2: I added a console.log(pageNum) to the fetchData-function, and tested a bit, and it seems that whatever I put into the initial value in useState(VALUE) is what is being printed. That makes NO sense to me. Help.

EDIT3: Added await, already had it in real code

EDIT4: I've tried at this for a while, but realized as I am using react that I could move the scroll-listener I have down to the JSX-area, and then it worked - for some reason. It now works. Can't really mark any answers as the correnct ones, but the problem is somewhat solved.

Thanks all who tried to help, really appreciate it.

Sebastian
  • 1,321
  • 9
  • 21
  • Could you create a sandbox or something that car recreate the issue so we can check the code and how does it work? – Ricardo Gonzalez Oct 17 '19 at 21:34
  • It looks like the condition in your condition inside `fetchData` is handling your page `0` as `false`, you should check if it is more or equal than 0 cause you are checking the number of the page not if the page exist – Ricardo Gonzalez Oct 17 '19 at 21:42

3 Answers3

1

Your staleness issues are occurring because React is not aware of your dependencies on the component state.

For example, useEffect ensures that value of showMoreRows from the scope of the initial render will be called on every scroll. This copy of showMoreRows refers to the initial value of pageNum, but that value is "frozen" in a closure along with the function and won't change when the component state does. Hence the scroll listening won't work as it needs to know the current state of pageNum.

You can resolve the issues by using callbacks to "hookify" showMoreRows and fetchData and declare their dependence on the component state. You must then declare the dependence of useEffect on these callbacks and use a clean-up function to handle the effect being invoked more than once.

It would look something like this (I haven't tried running the code):

import React, { useState } from 'react' 

function Table(props) {
  const [ cities, setCities ] = useState([])
  const [ pageNum, setPageNum ] = useState(0)

  useEffect(()=> { 
    // Run this only once by not declaring any dependencies
    fetchData(0)
  }, [])

  useEffect(()=> {
    // This will run whenever showMoreRows changes.

    const onScroll = () => {
      if (Math.ceil(table.scrollHeight - table.scrollTop) === table.clientHeight) showMoreRows()
    };

    document.querySelector(".TableComponent").onscroll = onScroll;

    // Clean-up
    return () => document.querySelector(".TableComponent").onscroll = null;
  }, [showMoreRows])

  const showMoreRows = React.useCallback(async function () {
    console.log("Show more rows!")
    await fetchData(pageNum)
  }, [fetchData, pageNum]);

  const fetchData = React.useCallback(async function (page) {
    // some code, describing fetching
    // EDIT2 start
    console.log(pageNum)
    // EDIT2 end
    const jsonResponse = await {}// THE RESPONSE FROM THE FETCH
    if(page) {
      setCities([...cities, ...jsonResponse])
      console.log("page is true", page)
      setPageNum(pageNum + 1)
    } else {
      console.log("page is false", page) // this always runs and prints "page is false 0"
      setCities([...cities, ...jsonResponse])
      setPageNum(1)
     }
  }, [setCities, cities, setPageNum, pageNum]);

  return <div className="TableComponent"> { pageNum } 
           <!-- The rest of the component --> 
         </div>
}
Ned Howley
  • 828
  • 11
  • 19
  • I also found this [React useState hook event handler using initial state](https://stackoverflow.com/questions/55265255/react-usestate-hook-event-handler-using-initial-state) which explains the problem – Sebastian Oct 18 '19 at 22:48
0

This might not totally solve the problem (it's hard to tell without more context), but useEffect runs every render, so things like that 'initial' fetchData(0) are going to run every update, which would probably give you the result from page = 0 every time in that conditional in fetchData.

  • 1
    That's a fair guess, but the empty array in the end of the useEffect should make the useEffect more similar to componentDidMount, in the sense that it should only run once, right after mount. Correct me if I'm wrong. – Sebastian Oct 18 '19 at 05:18
  • I also added a check: `if(citites.length === 0) fetchData(0)`, but this didn't change anything, so I don't believe the problem is that. – Sebastian Oct 18 '19 at 05:19
0

It's hard to say without more context, but I have one guess. Try using

setCities(value => [...value, ...jsonResponse])

instead of

setCities([...cities, ...jsonResponse])

Also make sure you use await for resolving promises for requests like:

const jsonResponse = await ...

You can console log it to check if they are not pending and that you get the right property if it's a nested object.

heaks
  • 77
  • 1
  • 6
  • In my actual code, I use awaits both for the `fetch` and the `response.json()`, but fair to point out as I hadn't put it in the skeleton-code. As for the setCities-function-parameter, I tried, but it seems that `cities` is being read as `[]` in the function, not as anything with any length, so the problem is that the function can't seem to read the state. – Sebastian Oct 18 '19 at 05:33