Virtual List, infinite scroll and list groups

I have a virtual list using infinite scroll in my React app, and I want to use list groups to group items in the list by date.
I have everything working, except that the virtual list only counts items in the first array (of grouped items), meaning that when it hits the last element in the list (say, the 9th group) it rerenders with a fromIndex of 10 and a toIndex of 9, and gets stuck in a loop or rerendering.
Is there a way of getting virtual lists to recognise groups so that it knows how many actual elements there are in total?

My code:

// @flow

import * as React from 'react'
import PropTypes from 'prop-types'
import {
  Page, NavRight, List, ListItem,
  Navbar, Link, Preloader, ListGroup,
  Searchbar, f7
} from 'framework7-react'
import { isEmpty } from 'lodash'
import { useTranslation } from 'react-i18next'
import { useSelector, useDispatch } from 'react-redux'
import { DateTime } from 'luxon'
import { loadJobHistory } from 'ducks/jobs'

type Props = {
}

let virtualList = {}

const JobHistory = (props: Props): React.Node => {
  const { t } = useTranslation()
  const dispatch = useDispatch()

  const driverID = useSelector(state => state.driver.driverObj.id)
  const jobHistoryByDate = useSelector(state => state.jobs.jobHistoryByDate)

  React.useEffect(() => {
    dispatch(loadJobHistory(driverID))
  }, [])

  const [vlData, setVLData] = React.useState({ items: [], topPosition: 0 })
  const [allowInfinite, setAllowInfinite] = React.useState(true)
  const [showPreloader, setShowPreloader] = React.useState(true)
  const [isSearching, setIsSearching] = React.useState(false)

  React.useEffect(() => {
    if (!isEmpty(virtualList) && !isSearching) {
      virtualList.replaceAllItems(jobHistoryByDate)
      setTimeout(() => setAllowInfinite(true), 1000)
    }
  }, [jobHistoryByDate])

  function searchAll (query, items) {
    const found = []
    for (let i = 0; i < items.length; i += 1) {
      if (items[i].pickupAddress.toLowerCase().indexOf(query.toLowerCase()) >= 0 ||
        items[i].destinationAddress.toLowerCase().indexOf(query.toLowerCase()) >= 0 ||
        query.trim() === '') {
          found.push(i)
        }
    }
    return found // return array with matched indexes
  }

  const renderExternal = (vl, vlData) => {
    virtualList = vl
    setVLData(vlData)
  }

  function loadMore () {
    if (!allowInfinite)
      { return }
    setAllowInfinite(false)
    dispatch(loadJobHistory(driverID, true))
  }
  return (
    <Page
      infinite
      infiniteDistance={100}
      infinitePreloader={showPreloader}
      onInfinite={loadMore}>
      <Navbar title='Job History' backLink='Back'>
        <NavRight>
          <Link searchbarEnable='.searchbar' iconIos='f7:search' iconAurora='f7:search' iconMd='material:search' />
        </NavRight>
        <Searchbar
          className='searchbar'
          expandable
          searchContainer='.virtual-list'
          searchItem='li'
          searchIn='.item-content'
          disableButton={!f7.theme.aurora}
          onSearchbarDisable={(searchbar) => {
            setIsSearching(false)
            setAllowInfinite(true)
            setShowPreloader(true)
          }}
          onSearchbarEnable={(searchbar) => {
            setIsSearching(true)
            setAllowInfinite(false)
            setShowPreloader(false)
          }}
        />
      </Navbar>
      <List className='searchbar-not-found'>
        <ListItem title={t('SearchNothingFound', 'Nothing found')} />
      </List>
      <List
        className='searchbar-found'
        mediaList
        virtualList
        virtualListParams={{ items: jobHistoryByDate, createUl: false, searchAll: searchAll, renderExternal: renderExternal, height: (f7.theme.ios ? 112 : (f7.theme.md ? 124 : 112)) }}
  >
          {vlData.items.length > 1 && vlData.items.map((group, index) => {
            const { date } = group
            return (
                <ListGroup mediaList key={index}>
                      <ListItem title={DateTime.fromISO(date).toLocaleString()} key={date} groupTitle mediaItem/>
            {
              group.items.map((item, index) => {
            const { startTime, pickupAddress, destinationAddress, fare } = item
            const jobStartTime = new Date(Date.parse(startTime))
            return (
              <ListItem
                key={item.id}
                mediaItem
                chevronCenter
                link='#'
                title={t('jobStartTime', { date: jobStartTime })}
                after={t('jobStartTimeDate', { date: jobStartTime })}
                style={{ top: `${vlData.topPosition}px` }}
              >
                <div slot='inner'>
                  <div className='job-history-address'>{t('jobFromLabel')} {pickupAddress}</div>
                  <div className='job-history-address'>{t('jobToLabel')} {destinationAddress}</div>
                  <div>£{fare}</div>
                </div>
              </ListItem>
            )})
          }
          </ListGroup>
          )})
        }
      </List>
    </Page>
  )
}

JobHistory.propTypes = ({

}: {...})

export default JobHistory

No, virtual list can work only with “flat” data. In this case the best can be done is to flat the data into single-level array with format like:

[
  {title: 'Group 1', groupTitle: true},
  {title: 'Item 1 1'},
  {title: 'Item 1 2'},
  ...
  {title: 'Group 2', groupTitle: true},
  {title: 'Item 2 1'},
  {title: 'Item 2 1'},
]

And render all of them in a single list, and to use <ListItem divider> instead of <ListItem groupTitle>

Ah right, I thought that might be the case, but thank you for confirming it.
Could I not use <ListItem groupTitle> to retain the sticky group title behaviour? Or does that only work within <ListGroup> elements?

You can try, but not sure it will work correctly because sticky items usually must be wrapped with something where they “stop” floating