'Apollo Client relayStylePagination doesn't paginate data leading to wrong component state

I've implemented a TableWithData component that creates a Table after fetching the data using a relayStylePagination logic

// index.tsx
const cache = new InMemoryCache({
  typePolicies: {
    Query: {
      fields: {
        tableData: relayStylePagination(),
      },
    },
  },
})



const GET_TABLE_DATA = gql`
  query GetTableData($first: Int, $after: String) {
    tableData(first: $first, after: $after) {
      totalCount
      edges {
        node
        cursor
      }
      pageInfo {
        endCursor
        hasNextPage
        startCursor
        hasPreviousPage
      }
    }
  }
`

interface TableWithDataProps {
  defaultPageSize?: number
}

export const TableWithData = ({ defaultPageSize = 10 }: TableWithDataProps) => {
  const {
    data: queryData,
    fetchMore,
    error,
    loading,
  } = useQuery<GetTableDataQuery, GetTableDataQueryVariables>(GET_TABLE_DATA, {
    variables: {
      first: defaultPageSize,
    },
    notifyOnNetworkStatusChange: true,
  })

  const edges = queryData?.tableData.edges.map((edge) => edge?.node)
  const pageInfo = queryData?.tableData.pageInfo
  const totalCount = queryData?.tableData.totalCount

  const [data, setData] = React.useState<any[]>([])
  const [pageCount, setPageCount] = React.useState(0)

  const columns = React.useMemo(
    () => [
      {
        Header: 'Code',
        accessor: 'code',
      },
      {
        Header: 'Promo',
        accessor: 'promo',
      },
      {
        Header: 'Type',
        accessor: 'type',
      },
    ],
    [],
  )

  React.useEffect(() => {
    if (loading) {
      return
    }

    if (error) {
      console.error(error)
      return
    }

    if (edges) {
      setData(edges)
      setPageCount(Math.ceil(totalCount ? totalCount / defaultPageSize : 0))
    }
  }, [loading])

  const fetchData = React.useCallback(
    ({ pageSize }) => {
      if (pageInfo?.hasNextPage) {
        fetchMore({
          variables: {
            first: pageSize,
            after: pageInfo.endCursor,
          },
        })
      }
    },
    [pageInfo],
  )

  return (
    <>
      <Table
        columns={columns}
        data={data}
        fetchData={fetchData}
        loading={loading}
        pageCount={pageCount}
      />
    </>
  )
}

However when the nextPage button in the Table is clicked, the pageIndex variables that holds the page currently displayed is reset to 0 and the fetchMore is called twice. I see from the debug console that apart from the first slice, the following two pages are fetched which data is added to the displayed one leading to a wrong state of the Table.

Here is the Table implementation

interface TableProps extends React.HTMLAttributes<HTMLTableElement> {
  columns?: any
  data?: any
  fetchData?: ({ pageSize }: { pageSize: number }) => void
  loading?: boolean
  pageCount?: any
}

const Table = ({
  columns,
  data,
  fetchData,
  loading,
  pageCount: controlledPageCount,
}: TableProps) => {
  const {
    getTableProps,
    getTableBodyProps,
    headerGroups,
    prepareRow,
    page,
    canPreviousPage,
    canNextPage,
    pageOptions,
    pageCount,
    gotoPage,
    nextPage,
    previousPage,
    setPageSize,
    state: { pageIndex, pageSize },
  } = useTable(
    {
      data,
      columns,
      initialState: { pageIndex: 0, pageSize: 10 },
      manualPagination: true,
      pageCount: controlledPageCount,
    },
    useSortBy,
    usePagination,
  )

  React.useEffect(() => {
    if (fetchData) fetchData({ pageSize })
  }, [pageIndex, pageSize])

  return (
    <>
      <pre>
        <code>
          {JSON.stringify(
            {
              pageIndex,
              pageSize,
              pageCount,
              canNextPage,
              canPreviousPage,
            },
            null,
            2,
          )}
        </code>
      </pre>
      <table {...getTableProps()}>
        <thead>
          {headerGroups.map((headerGroup) => {
            const { key, ...restHeaderGroupProps } =
              headerGroup.getHeaderGroupProps()
            return (
              <tr key={key} {...restHeaderGroupProps}>
                {headerGroup.headers.map((column) => {
                  const { key, ...restColumn } = column.getHeaderProps(
                    column.getSortByToggleProps(),
                  )
                  return (
                    <th key={key} {...restColumn}>
                      {column.render('Header')}
                      <span>
                        {column.isSorted
                          ? column.isSortedDesc
                            ? ' 🔽'
                            : ' 🔼'
                          : ''}
                      </span>
                    </th>
                  )
                })}
              </tr>
            )
          })}
        </thead>
        <tbody {...getTableBodyProps()}>
          {page.map((row: Row<object>, i: number) => {
            prepareRow(row)
            const { key, ...restRowProps } = row.getRowProps()
            return (
              <tr key={key} {...restRowProps}>
                {row.cells.map((cell) => {
                  const { key, ...restCellProps } = cell.getCellProps()
                  return (
                    <td key={key} {...restCellProps}>
                      {cell.render('Cell')}
                    </td>
                  )
                })}
              </tr>
            )
          })}
          <tr>
            {loading ? (
              // Use our custom loading state to show a loading indicator
              <td>Loading...</td>
            ) : (
              <td>
                Showing {page.length} of ~{controlledPageCount * pageSize}{' '}
                results
              </td>
            )}
          </tr>
        </tbody>
      </table>
      <div className="pagination">
        <button onClick={() => gotoPage(0)} disabled={!canPreviousPage}>
          {'<<'}
        </button>{' '}
        <button onClick={() => previousPage()} disabled={!canPreviousPage}>
          {'<'}
        </button>{' '}
        <button onClick={() => nextPage()} disabled={!canNextPage}>
          {'>'}
        </button>{' '}
        <button onClick={() => gotoPage(pageCount - 1)} disabled={!canNextPage}>
          {'>>'}
        </button>{' '}
        <span>
          Page{' '}
          <strong>
            {pageIndex + 1} of {pageOptions.length}
          </strong>{' '}
        </span>
        <span>
          | Go to page:{' '}
          <input
            type="number"
            defaultValue={pageIndex + 1}
            onChange={(e) => {
              const page = e.target.value ? Number(e.target.value) - 1 : 0
              gotoPage(page)
            }}
            style={{ width: '100px' }}
          />
        </span>{' '}
        <select
          value={pageSize}
          onChange={(e) => {
            setPageSize(Number(e.target.value))
          }}
        >
          {[10, 20, 30, 40, 50].map((pageSize) => (
            <option key={pageSize} value={pageSize}>
              Show {pageSize}
            </option>
          ))}
        </select>
      </div>
    </>
  )
}

Why is the pageIndex being reset to 0? Why the fetchMore is called twice? And why are the fetched data from the query being added to the previous one?

Here is a clip of the issue

enter image description here



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source